FlowAct-R2 / static /js /blog.js
ProAudience's picture
Deploy FlowAct-R2 static site
16ed0c5 verified
Raw History Blame Contribute Delete
5.93 kB
'use strict';
// Native video controls own all pointer/drag events; gallery navigation never intercepts them.
const videos = Array.from(document.querySelectorAll('video'));
function playVideo(video) {
const shell = video.closest('.video-shell');
const error = shell.querySelector('.video-error');
error.hidden = true;
if (!video.dataset.loaded || video.error) {
video.querySelectorAll('source[data-src]').forEach(source => { source.src = source.dataset.src; });
video.dataset.loaded = 'true';
video.load();
}
shell.classList.add('is-loaded');
video.muted = false;
video.volume = 1;
video.play().catch(reason => {
if (reason.name === 'AbortError') return;
shell.classList.remove('is-loaded');
error.textContent = 'Playback could not start. Please try again.';
error.hidden = false;
});
}
videos.forEach(video => {
const shell = video.closest('.video-shell');
shell.querySelector('.video-play').addEventListener('click', () => playVideo(video));
video.addEventListener('click', () => { if (!video.dataset.loaded) playVideo(video); });
video.addEventListener('error', () => {
shell.classList.remove('is-loaded');
const error = shell.querySelector('.video-error');
error.textContent = 'Video unavailable. Select play to retry.';
error.hidden = false;
});
});
document.addEventListener('play', event => {
if (!(event.target instanceof HTMLVideoElement)) return;
videos.forEach(video => { if (video !== event.target && !video.paused) video.pause(); });
}, true);
if ('IntersectionObserver' in window) {
const posters = new IntersectionObserver(entries => {
entries.forEach(entry => {
if (!entry.isIntersecting) return;
const video = entry.target;
video.poster = video.dataset.poster;
posters.unobserve(video);
});
}, { rootMargin: '400px' });
videos.filter(video => video.dataset.poster).forEach(video => posters.observe(video));
} else {
videos.filter(video => video.dataset.poster).forEach(video => { video.poster = video.dataset.poster; });
}
const reducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)');
document.querySelectorAll('[data-gallery]').forEach(gallery => {
const track = gallery.querySelector('.gallery-track');
const items = Array.from(track.children);
const previous = gallery.querySelector('[data-direction="-1"]');
const next = gallery.querySelector('[data-direction="1"]');
const counter = gallery.querySelector('.gallery-counter');
previous.setAttribute('aria-controls', track.id);
next.setAttribute('aria-controls', track.id);
function update() {
const box = track.getBoundingClientRect();
const visible = items.map((item, index) => ({ box: item.getBoundingClientRect(), index }))
.filter(item => item.box.right > box.left + 3 && item.box.left < box.right - 3);
if (visible.length) counter.textContent = (visible[0].index + 1) + '-' + (visible.at(-1).index + 1) + ' / ' + items.length;
previous.disabled = track.scrollLeft < 3;
next.disabled = track.scrollLeft >= track.scrollWidth - track.clientWidth - 3;
}
function navigate(direction) {
const stride = items[0].getBoundingClientRect().width + parseFloat(getComputedStyle(track).gap);
const step = Math.max(1, Math.round(track.clientWidth / stride));
const index = Math.round(track.scrollLeft / stride);
track.scrollTo({ left: Math.max(0, (index + direction * step) * stride), behavior: reducedMotion.matches ? 'instant' : 'smooth' });
}
previous.addEventListener('click', () => navigate(-1));
next.addEventListener('click', () => navigate(1));
track.addEventListener('keydown', event => {
if (event.target !== track) return;
if (event.key === 'ArrowLeft' || event.key === 'ArrowRight') {
event.preventDefault();
navigate(event.key === 'ArrowLeft' ? -1 : 1);
}
});
track.addEventListener('scroll', update, { passive: true });
if ('ResizeObserver' in window) new ResizeObserver(update).observe(track);
else window.addEventListener('resize', update, { passive: true });
update();
});
const sections = Array.from(document.querySelectorAll('.article-section'));
const links = Array.from(document.querySelectorAll('.contents a[href]'));
let scheduled = false;
function markCurrentSection() {
scheduled = false;
let current = sections[0];
sections.forEach(section => { if (section.getBoundingClientRect().top <= 160) current = section; });
links.forEach(link => {
if (link.hash === '#' + current.id) link.setAttribute('aria-current', 'location');
else link.removeAttribute('aria-current');
});
}
window.addEventListener('scroll', () => {
if (!scheduled) { scheduled = true; requestAnimationFrame(markCurrentSection); }
}, { passive: true });
markCurrentSection();
document.querySelector('.copy-citation').addEventListener('click', async () => {
const code = document.querySelector('#citation-code');
const status = document.querySelector('.copy-status');
try {
if (navigator.clipboard && window.isSecureContext) {
await navigator.clipboard.writeText(code.textContent);
} else {
// Legacy fallback supports the project's plain-HTTP LAN preview.
const field = document.createElement('textarea');
field.value = code.textContent;
field.style.cssText = 'position:fixed;left:-9999px;top:0';
document.body.appendChild(field);
field.select();
const copied = document.execCommand('copy');
field.remove();
document.querySelector('.copy-citation').focus();
if (!copied) throw new Error('Clipboard unavailable');
}
status.textContent = 'Copied.';
} catch (error) {
const range = document.createRange();
range.selectNodeContents(code);
const selection = window.getSelection();
selection.removeAllRanges();
selection.addRange(range);
status.textContent = 'Could not copy. Citation text selected.';
}
});