Files
intotheeast-com-content/themes/intotheeast/js/src/main.js
T

297 lines
13 KiB
JavaScript
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.
/* ── Fonts ───────────────────────────────────────────────── */
import '@fontsource-variable/dm-sans';
import '@fontsource/dm-serif-display/400.css';
import '@fontsource/dm-serif-display/400-italic.css';
/* ── PhotoSwipe ──────────────────────────────────────────── */
import PhotoSwipeLightbox from 'photoswipe/lightbox';
import PhotoSwipe from 'photoswipe';
import 'photoswipe/style.css';
/* ── Scrollama (used by story.html.twig inline script) ────── */
import scrollama from 'scrollama';
window.scrollama = scrollama;
/* ── Photo strip: prev/next buttons + scroll-based dot sync ─ */
function initPhotoStrip() {
document.querySelectorAll('.journal-photo-strip').forEach(function (strip) {
strip.setAttribute('role', 'region');
strip.setAttribute('aria-label', 'Photo strip');
strip.setAttribute('tabindex', '0');
var slideCount = parseInt(strip.dataset.slides, 10) || 1;
var dots = strip.nextElementSibling;
if (!dots || !dots.classList.contains('journal-photo-dots')) return;
var dotEls = Array.from(dots.querySelectorAll('.journal-photo-dot'));
strip.addEventListener('scroll', function () {
var idx = Math.round(strip.scrollLeft / strip.offsetWidth);
dotEls.forEach(function (d, i) { d.classList.toggle('is-active', i === idx); });
}, { passive: true });
if (slideCount < 2) return;
var prev = document.createElement('button');
prev.className = 'strip-prev';
prev.setAttribute('aria-label', 'Previous photo');
prev.textContent = '';
prev.addEventListener('click', function () {
strip.scrollBy({ left: -strip.offsetWidth, behavior: 'smooth' });
});
var next = document.createElement('button');
next.className = 'strip-next';
next.setAttribute('aria-label', 'Next photo');
next.textContent = '';
next.addEventListener('click', function () {
strip.scrollBy({ left: strip.offsetWidth, behavior: 'smooth' });
});
var controls = document.createElement('div');
controls.className = 'strip-controls';
controls.appendChild(prev);
controls.appendChild(next);
var wrap = strip.closest('.journal-photo-wrap');
(wrap || dots).insertAdjacentElement('afterend', controls);
});
}
/* ── PhotoSwipe lightbox + IntersectionObserver dot sync ──── */
function initPhotoSwipe() {
if (!document.querySelector('.pswp-gallery')) return;
var lightbox = new PhotoSwipeLightbox({
gallery: '.pswp-gallery',
children: 'a.journal-photo-slide',
pswpModule: PhotoSwipe
});
lightbox.on('afterOpen', function () {
var pswp = lightbox.pswp;
var keyDir = 0;
var clearTimer = null;
function onKey(e) {
if (e.key === 'ArrowRight') keyDir = 1;
else if (e.key === 'ArrowLeft') keyDir = -1;
else keyDir = 0;
}
document.addEventListener('keydown', onKey, true);
pswp.on('change', function () {
if (!keyDir) return;
var dir = keyDir;
keyDir = 0;
var el = pswp.currSlide && pswp.currSlide.container;
if (!el) return;
el.classList.remove('pswp-key-from-left', 'pswp-key-from-right');
el.offsetWidth; /* force reflow */
el.classList.add(dir > 0 ? 'pswp-key-from-right' : 'pswp-key-from-left');
clearTimeout(clearTimer);
clearTimer = setTimeout(function () {
el.classList.remove('pswp-key-from-left', 'pswp-key-from-right');
}, 400);
});
pswp.on('close', function () {
document.removeEventListener('keydown', onKey, true);
clearTimeout(clearTimer);
});
});
lightbox.init();
/* Per-strip: IntersectionObserver dot sync + expand button */
document.querySelectorAll('.journal-photo-wrap').forEach(function (wrap) {
var strip = wrap.querySelector('.journal-photo-strip');
if (!strip) return;
var slides = Array.from(strip.querySelectorAll('a.journal-photo-slide'));
var expandBtn = wrap.querySelector('.journal-photo-expand');
var article = wrap.closest('article');
var dots = article ? Array.from(article.querySelectorAll('.journal-photo-dot')) : [];
var visibleIdx = 0;
var io = new IntersectionObserver(function (entries) {
entries.forEach(function (e) {
if (!e.isIntersecting) return;
visibleIdx = slides.indexOf(e.target);
dots.forEach(function (d) { d.classList.remove('is-active'); });
if (dots[visibleIdx]) dots[visibleIdx].classList.add('is-active');
});
}, { root: strip, threshold: 0.5 });
slides.forEach(function (s) { io.observe(s); });
if (expandBtn && slides.length) {
expandBtn.addEventListener('click', function () {
slides[visibleIdx].dispatchEvent(
new MouseEvent('click', { bubbles: true, cancelable: true })
);
});
}
});
}
/* ── Sort button ─────────────────────────────────────────────
btnId: element id of the sort toggle button
containerSel: CSS selector for the list container
itemSel: CSS selector for sortable items within container
withLabel: true = button shows "↑ Oldest first" / "↓ Newest first"
false = button shows "↑" / "↓" only
──────────────────────────────────────────────────────────── */
function initSortButton(btnId, containerSel, itemSel, withLabel) {
var btn = document.getElementById(btnId);
if (!btn) return;
var container = document.querySelector(containerSel);
if (!container) return;
var sentinel = container.querySelector('#feed-filter-empty');
var ascending = true;
btn.addEventListener('click', function () {
ascending = !ascending;
var items = Array.from(container.querySelectorAll(itemSel));
items.reverse().forEach(function (el) {
if (sentinel) container.insertBefore(el, sentinel);
else container.appendChild(el);
});
btn.textContent = withLabel
? (ascending ? '↑ Oldest first' : '↓ Newest first')
: (ascending ? '↑' : '↓');
btn.setAttribute('aria-label', ascending ? 'Sort: oldest first' : 'Sort: newest first');
btn.classList.toggle('is-active', !ascending);
});
}
/* ── Filter bar (trip page: All / Journal / Stories) ─────── */
function initFilterBar() {
var filterBtns = document.querySelectorAll('.trip-filter-btn');
if (!filterBtns.length) return;
var cards = document.querySelectorAll('[data-type]');
var filterEmpty = document.getElementById('feed-filter-empty');
filterBtns.forEach(function (btn) {
btn.addEventListener('click', function () {
filterBtns.forEach(function (b) {
b.classList.remove('is-active');
b.setAttribute('aria-pressed', 'false');
});
btn.classList.add('is-active');
btn.setAttribute('aria-pressed', 'true');
var filter = btn.getAttribute('data-filter');
var visible = 0;
cards.forEach(function (card) {
var show = filter === 'all' || card.getAttribute('data-type') === filter;
card.style.display = show ? '' : 'none';
if (show) visible++;
});
if (filterEmpty) {
if (visible === 0) {
filterEmpty.textContent = filter === 'story'
? 'No stories yet for this trip.'
: 'No entries yet.';
filterEmpty.style.display = '';
} else {
filterEmpty.style.display = 'none';
}
}
});
});
}
/* ── Back to top ─────────────────────────────────────────── */
function initBackToTop(btnId) {
var btn = document.getElementById(btnId);
if (!btn) return;
var threshold = window.innerHeight * 0.8;
var shown = false;
btn.addEventListener('click', function () {
history.pushState(null, '', window.location.pathname + window.location.search);
window.scrollTo({ top: 0, behavior: 'smooth' });
});
window.addEventListener('scroll', function () {
var shouldShow = window.scrollY > threshold;
if (shouldShow !== shown) {
shown = shouldShow;
btn.classList.toggle('is-visible', shown);
}
}, { passive: true });
}
/* ── Panel toggles (trip stats / cycling panels) ─────────── */
function initPanelToggles() {
document.querySelectorAll('.trip-panel-toggle').forEach(function (toggle) {
var blockId = toggle.getAttribute('aria-controls');
var block = blockId ? document.getElementById(blockId) : null;
if (!block) return;
toggle.addEventListener('click', function () {
var isOpen = block.classList.contains('is-open');
block.classList.toggle('is-open', !isOpen);
toggle.classList.toggle('is-active', !isOpen);
toggle.setAttribute('aria-expanded', isOpen ? 'false' : 'true');
});
});
document.querySelectorAll('.trip-panel-close').forEach(function (btn) {
var toggleBtn = document.getElementById(btn.getAttribute('data-toggle'));
if (toggleBtn) btn.addEventListener('click', function () { toggleBtn.click(); });
});
}
/* ── Trip stats / cycling computation (trip + home-active) ───
config: { gpxUrls: [], gpsPoints: [[lat,lng],...], hasGpx: bool }
No-op if #stat-distance is absent (page rendered no stats panel).
No-GPX fallback: if gpsPoints.length < 2, write '—' and return (no '~0'). */
function initTripStats(config) {
var distEl = document.getElementById('stat-distance');
if (!distEl) return;
var gpxUrls = config.gpxUrls || [];
var gpsPoints = config.gpsPoints || [];
if (config.hasGpx) {
MapUtils.parseGpxFiles(gpxUrls, function (result) {
distEl.textContent = result.distance > 0 ? Math.round(result.distance).toLocaleString() : '—';
function setText(id, val) {
var el = document.getElementById(id);
if (el) el.textContent = val;
}
setText('cyc-distance', result.distance > 0 ? Math.round(result.distance).toLocaleString() : '—');
setText('cyc-ele-gain', !isNaN(result.eleGain) ? Math.round(result.eleGain) : '—');
setText('cyc-ele-loss', !isNaN(result.eleLoss) ? Math.round(result.eleLoss) : '—');
setText('cyc-highest', !isNaN(result.highest) ? Math.round(result.highest) : '—');
setText('cyc-lowest', !isNaN(result.lowest) ? Math.round(result.lowest) : '—');
setText('cyc-moving-time', result.movingTime || '—');
setText('cyc-avg-speed', result.avgSpeed > 0 ? result.avgSpeed.toFixed(1) : '—');
});
} else {
if (gpsPoints.length < 2) {
distEl.textContent = '—';
return;
}
var total = 0;
for (var i = 1; i < gpsPoints.length; i++) {
total += MapUtils.haversineKm(
parseFloat(gpsPoints[i-1][0]), parseFloat(gpsPoints[i-1][1]),
parseFloat(gpsPoints[i][0]), parseFloat(gpsPoints[i][1])
);
}
distEl.textContent = '~' + Math.round(total).toLocaleString();
}
}
window.initTripStats = initTripStats;
/* ── Boot ────────────────────────────────────────────────── */
document.addEventListener('DOMContentLoaded', function () {
initPhotoStrip();
initPhotoSwipe();
initFilterBar();
/* Sort buttons — each call is silent if its button/container isn't on this page */
initSortButton('trip-sort-toggle', '.feed', '[data-type]', false);
initSortButton('feed-sort-toggle', '.feed', '[data-type]', true);
initSortButton('feed-sort-toggle', '.stories-grid', '.story-card', true);
initBackToTop('story-totop');
initBackToTop('trip-totop');
initPanelToggles();
});