File size: 5,932 Bytes
16ed0c5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
'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.';
  }
});