1import { $, $$ } from "./dom.js";
2
3export const initPhotoPreview = (signal) => {
4 const dialog = $("#photo-preview");
5 if (!dialog) return;
6 const img = $("img", dialog);
7 const hint = $(".photo-preview__hint", dialog);
8
9 const items = [
10 ...$$(".photo-grid__item a").map((el) => ({ el, src: el.href })),
11 ...$$(".article-content img").map((el) => ({ el, src: el.dataset.full || el.src })),
12 ];
13 if (!items.length) return;
14
15 const sources = items.map((it) => it.src);
16 const multiple = sources.length > 1;
17 let current = 0;
18
19 if (hint) hint.hidden = !multiple;
20
21 const show = (index) => {
22 current = (index + sources.length) % sources.length;
23 const src = sources[current];
24 if (img.getAttribute("src") !== src) {
25 img.removeAttribute("src");
26 img.style.visibility = "hidden";
27 img.onload = () => { img.style.visibility = "visible"; };
28 img.src = src;
29 }
30 };
31
32 const openAt = (index) => {
33 show(index);
34 dialog.showModal();
35 history.pushState(null, "", "#preview-open");
36 };
37 const step = (delta) => { if (multiple) show(current + delta); };
38
39 items.forEach((it, i) => {
40 if (it.el.tagName === "A") {
41 it.el.addEventListener("click", (e) => { e.preventDefault(); openAt(i); }, { signal });
42 } else {
43 it.el.style.cursor = "zoom-in";
44 it.el.addEventListener("click", () => openAt(i), { signal });
45 }
46 });
47
48 $("[data-preview-close]", dialog)?.addEventListener("click", () => dialog.close(), { signal });
49
50 // When dialog closes by any means other than Back, pop the history state we pushed.
51 dialog.addEventListener("close", () => {
52 if (location.hash === "#preview-open") history.back();
53 }, { signal });
54
55 // Back button / mobile swipe-back: close the dialog instead of navigating away.
56 window.addEventListener("popstate", () => {
57 if (dialog.open) dialog.close();
58 }, { signal });
59
60 document.addEventListener("keydown", (e) => {
61 if (!dialog.open) return;
62 if (e.key === "Enter") { dialog.close(); return; }
63 if (!multiple) return;
64 if (e.key === "ArrowLeft") step(-1);
65 else if (e.key === "ArrowRight") step(1);
66 }, { signal });
67
68 // Click image: navigate left/right half; close if single.
69 img.addEventListener("click", (e) => {
70 if (!multiple) { dialog.close(); return; }
71 step(e.offsetX < img.offsetWidth / 2 ? -1 : 1);
72 }, { signal });
73
74 // Backdrop click closes.
75 dialog.addEventListener("click", (e) => {
76 if (e.target === dialog) dialog.close();
77 }, { signal });
78
79 // Scroll wheel navigation (throttled to one step per 300ms).
80 let wheelLocked = false;
81 dialog.addEventListener("wheel", (e) => {
82 if (!multiple || wheelLocked) return;
83 wheelLocked = true;
84 step(e.deltaY > 0 ? 1 : -1);
85 setTimeout(() => { wheelLocked = false; }, 300);
86 }, { passive: true, signal });
87
88 // Swipe navigation. Ignore multi-touch (pinch-to-zoom) so zooming doesn't step.
89 let touchX = 0;
90 let pinching = false;
91 dialog.addEventListener("touchstart", (e) => {
92 pinching = e.touches.length > 1;
93 touchX = e.touches[0].clientX;
94 }, { passive: true, signal });
95 dialog.addEventListener("touchend", (e) => {
96 if (pinching || e.touches.length > 0) return;
97 const dx = e.changedTouches[0].clientX - touchX;
98 if (Math.abs(dx) > 50) step(dx < 0 ? 1 : -1);
99 }, { signal });
100};