@gallerist/renderer-dom 0.0.0-stage → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs ADDED
@@ -0,0 +1,1965 @@
1
+ 'use strict';
2
+
3
+ var core = require('@gallerist/core');
4
+ var internal = require('@gallerist/core/internal');
5
+
6
+ // src/adapters/image.ts
7
+ var COLOR_RE = /^(#|rgb|hsl|[a-z]+$)/i;
8
+ var CSS_URL_UNSAFE = /["\\\n\r]/;
9
+ var BROKEN_ICON = '<svg viewBox="0 0 24 24" width="44" height="44" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M10.4 10.4a2 2 0 1 1-2.8-2.8"/><path d="M21 15.5V5a2 2 0 0 0-2-2H8.5"/><path d="M3.6 3.6A2 2 0 0 0 3 5v14a2 2 0 0 0 2 2h14a2 2 0 0 0 1.4-.6"/><path d="m5 21 7-7"/><path d="m2 2 20 20"/></svg>';
10
+ function applyPlaceholder(el2, placeholder) {
11
+ if (placeholder.startsWith("data:") || placeholder.includes("/")) {
12
+ if (CSS_URL_UNSAFE.test(placeholder)) return;
13
+ el2.style.backgroundImage = `url("${placeholder}")`;
14
+ el2.style.backgroundSize = "cover";
15
+ el2.style.backgroundPosition = "center";
16
+ } else if (COLOR_RE.test(placeholder)) {
17
+ el2.style.backgroundColor = placeholder;
18
+ }
19
+ }
20
+ function imageAdapter() {
21
+ return { type: "image", mount: mountImage };
22
+ }
23
+ function mountImage(item, slot, ctx) {
24
+ const img = document.createElement("img");
25
+ img.setAttribute("decoding", "async");
26
+ img.setAttribute("fetchpriority", ctx.priority ?? "auto");
27
+ img.className = "gallerist-media gallerist-media-image";
28
+ if (item.alt !== void 0) img.alt = item.alt;
29
+ if (item.placeholder) applyPlaceholder(img, item.placeholder);
30
+ slot.appendChild(img);
31
+ let element = img;
32
+ let errorEl = null;
33
+ const showError = () => {
34
+ if (errorEl) return;
35
+ img.setAttribute("data-gallerist-error", "");
36
+ const card = document.createElement("div");
37
+ card.className = "gallerist-media-error";
38
+ card.setAttribute("role", "img");
39
+ if (item.alt) card.setAttribute("aria-label", item.alt);
40
+ const icon = document.createElement("div");
41
+ icon.className = "gallerist-media-error-icon";
42
+ icon.setAttribute("aria-hidden", "true");
43
+ icon.innerHTML = BROKEN_ICON;
44
+ card.appendChild(icon);
45
+ if (item.alt) {
46
+ const label = document.createElement("span");
47
+ label.className = "gallerist-media-error-label";
48
+ label.textContent = item.alt;
49
+ card.appendChild(label);
50
+ }
51
+ slot.appendChild(card);
52
+ errorEl = card;
53
+ };
54
+ const ready = new Promise((resolve, reject) => {
55
+ const abortErr = () => new DOMException("Image load aborted", "AbortError");
56
+ if (ctx.signal.aborted) {
57
+ reject(abortErr());
58
+ return;
59
+ }
60
+ const cleanup = () => {
61
+ img.removeEventListener("load", onLoad);
62
+ img.removeEventListener("error", onError);
63
+ ctx.signal.removeEventListener("abort", onAbort);
64
+ };
65
+ const onAbort = () => {
66
+ cleanup();
67
+ reject(abortErr());
68
+ };
69
+ const onLoad = async () => {
70
+ try {
71
+ if (typeof img.decode === "function") await img.decode();
72
+ } catch {
73
+ }
74
+ cleanup();
75
+ img.setAttribute("data-gallerist-loaded", "");
76
+ resolve();
77
+ };
78
+ const onError = () => {
79
+ cleanup();
80
+ showError();
81
+ reject(new Error(`image load error: ${item.id}`));
82
+ };
83
+ img.addEventListener("load", onLoad);
84
+ img.addEventListener("error", onError);
85
+ ctx.signal.addEventListener("abort", onAbort);
86
+ const src = item.source;
87
+ if (typeof src === "string") {
88
+ img.src = src;
89
+ } else if ("src" in src) {
90
+ img.src = src.src;
91
+ if (src.srcset) img.setAttribute("srcset", src.srcset);
92
+ if (src.sizes) img.setAttribute("sizes", src.sizes);
93
+ } else if ("loader" in src) {
94
+ src.loader(ctx.signal).then((res) => {
95
+ if (ctx.signal.aborted) return;
96
+ if (typeof res === "string") {
97
+ img.src = res;
98
+ } else {
99
+ cleanup();
100
+ img.remove();
101
+ slot.appendChild(res);
102
+ res.setAttribute("data-gallerist-loaded", "");
103
+ element = res;
104
+ resolve();
105
+ }
106
+ }).catch((err) => {
107
+ cleanup();
108
+ showError();
109
+ reject(err instanceof Error ? err : new Error(String(err)));
110
+ });
111
+ }
112
+ });
113
+ ready.catch(() => {
114
+ });
115
+ return {
116
+ get element() {
117
+ return element;
118
+ },
119
+ ready,
120
+ supportsZoom: true,
121
+ naturalSize() {
122
+ if (item.naturalSize) return item.naturalSize;
123
+ return img.naturalWidth ? { w: img.naturalWidth, h: img.naturalHeight } : null;
124
+ },
125
+ destroy() {
126
+ img.removeAttribute("src");
127
+ element.remove();
128
+ errorEl?.remove();
129
+ }
130
+ };
131
+ }
132
+
133
+ // src/appearance.ts
134
+ function applyAppearance(root, appearance) {
135
+ if ("skin" in appearance) root.setAttribute("data-gallerist-skin", appearance.skin ?? "gallery");
136
+ if ("scheme" in appearance) {
137
+ if (appearance.scheme) root.setAttribute("data-gallerist-scheme", appearance.scheme);
138
+ else root.removeAttribute("data-gallerist-scheme");
139
+ }
140
+ if ("accent" in appearance) {
141
+ if (appearance.accent) root.setAttribute("data-gallerist-accent", appearance.accent);
142
+ else root.removeAttribute("data-gallerist-accent");
143
+ }
144
+ }
145
+
146
+ // src/transform.ts
147
+ function zoomTransform(zoom) {
148
+ return `translate(${zoom.x}px, ${zoom.y}px) scale(${zoom.scale})`;
149
+ }
150
+
151
+ // src/gestures.ts
152
+ var dist = (a, b) => Math.hypot(a.x - b.x, a.y - b.y);
153
+ var mid = (a, b) => ({ x: (a.x + b.x) / 2, y: (a.y + b.y) / 2 });
154
+ function attachGestures(opts) {
155
+ const {
156
+ stage,
157
+ viewer,
158
+ getContent,
159
+ getGeometry,
160
+ threshold = 10,
161
+ commitNav = 60,
162
+ closeDismiss = 120,
163
+ maxScale = 4,
164
+ nav = {
165
+ next: () => viewer.next(),
166
+ prev: () => viewer.prev(),
167
+ nextItem: () => viewer.nextItem(),
168
+ prevItem: () => viewer.prevItem()
169
+ },
170
+ raf = (cb) => requestAnimationFrame(cb),
171
+ caf = (id) => cancelAnimationFrame(id)
172
+ } = opts;
173
+ const dismissEl = stage.closest(".gallerist") ?? stage;
174
+ const pointers = /* @__PURE__ */ new Map();
175
+ let start = null;
176
+ let startZoom = { scale: 1, x: 0, y: 0 };
177
+ let pinchStartDist = 0;
178
+ let intent = "none";
179
+ let live = { scale: 1, x: 0, y: 0 };
180
+ let frame = 0;
181
+ let gestureGeom = null;
182
+ const geom = () => gestureGeom ?? getGeometry();
183
+ const fitOf = (g) => g.naturalSize ? internal.fitScale({ w: g.stageRect.width, h: g.stageRect.height }, g.naturalSize) : 1;
184
+ const fit = () => fitOf(geom());
185
+ const toStage = (p) => {
186
+ const r = geom().stageRect;
187
+ return { x: p.x - r.left, y: p.y - r.top };
188
+ };
189
+ const settle = (z, g = geom()) => {
190
+ const scaled = { scale: internal.clampScale(z.scale, fitOf(g), maxScale), x: z.x, y: z.y };
191
+ const stageSize = { w: g.stageRect.width, h: g.stageRect.height };
192
+ if (g.mediaBox) return internal.clampPanToBox(scaled, stageSize, g.mediaBox);
193
+ return g.naturalSize ? internal.clampPan(scaled, stageSize, g.naturalSize) : scaled;
194
+ };
195
+ const internals = internal.getInternals(viewer);
196
+ const commit = (z) => {
197
+ internals.setZoom(z);
198
+ const content = getContent();
199
+ if (content) content.style.transform = zoomTransform(viewer.getState().zoom);
200
+ };
201
+ const schedule = () => {
202
+ if (frame) return;
203
+ frame = raf(() => {
204
+ frame = 0;
205
+ const content = getContent();
206
+ if (content) {
207
+ content.style.transform = zoomTransform(live);
208
+ }
209
+ });
210
+ };
211
+ const capture = (id) => {
212
+ try {
213
+ stage.setPointerCapture?.(id);
214
+ } catch {
215
+ }
216
+ };
217
+ const release = (id) => {
218
+ try {
219
+ stage.releasePointerCapture?.(id);
220
+ } catch {
221
+ }
222
+ };
223
+ const WHEEL_COMMIT_MS = 90;
224
+ let wheelGeom = null;
225
+ let wheelZoom = null;
226
+ let wheelTimer;
227
+ const commitWheel = () => {
228
+ wheelTimer = void 0;
229
+ wheelGeom = null;
230
+ if (wheelZoom) {
231
+ commit(wheelZoom);
232
+ wheelZoom = null;
233
+ }
234
+ };
235
+ const flushWheel = () => {
236
+ if (wheelTimer) {
237
+ clearTimeout(wheelTimer);
238
+ commitWheel();
239
+ }
240
+ };
241
+ const onDown = (e) => {
242
+ flushWheel();
243
+ pointers.set(e.pointerId, { x: e.clientX, y: e.clientY });
244
+ startZoom = { ...viewer.getState().zoom };
245
+ live = { ...startZoom };
246
+ gestureGeom = getGeometry();
247
+ if (pointers.size === 1) {
248
+ start = { x: e.clientX, y: e.clientY };
249
+ intent = "none";
250
+ } else if (pointers.size === 2) {
251
+ const [a, b] = [...pointers.values()];
252
+ if (a && b) pinchStartDist = dist(a, b);
253
+ intent = "zoom";
254
+ for (const id of pointers.keys()) capture(id);
255
+ }
256
+ };
257
+ const resolveIntent = (dx, dy) => {
258
+ const g = geom();
259
+ return internal.arbitrate({
260
+ pointerCount: pointers.size === 0 ? 1 : pointers.size,
261
+ dx,
262
+ dy,
263
+ scale: startZoom.scale,
264
+ fitScale: g.naturalSize ? fit() : startZoom.scale,
265
+ grouped: currentGroupHasMultiple(viewer),
266
+ threshold
267
+ });
268
+ };
269
+ const onMove = (e) => {
270
+ if (!pointers.has(e.pointerId)) return;
271
+ pointers.set(e.pointerId, { x: e.clientX, y: e.clientY });
272
+ if (intent === "zoom" && pointers.size >= 2) {
273
+ const [a, b] = [...pointers.values()];
274
+ if (!a || !b || pinchStartDist === 0) return;
275
+ const factor = dist(a, b) / pinchStartDist;
276
+ const newScale = internal.clampScale(startZoom.scale * factor, fit(), maxScale);
277
+ live = internal.focalZoom(startZoom, toStage(mid(a, b)), newScale);
278
+ schedule();
279
+ return;
280
+ }
281
+ if (!start) return;
282
+ const dx = e.clientX - start.x;
283
+ const dy = e.clientY - start.y;
284
+ if (intent === "none") {
285
+ intent = resolveIntent(dx, dy);
286
+ if (intent !== "none") capture(e.pointerId);
287
+ }
288
+ if (intent === "pan") {
289
+ live = { scale: startZoom.scale, x: startZoom.x + dx, y: startZoom.y + dy };
290
+ schedule();
291
+ } else if (intent === "close") {
292
+ const k = Math.max(0.5, 1 - Math.abs(dy) / 1e3);
293
+ live = { scale: startZoom.scale * k, x: startZoom.x + dx, y: startZoom.y + dy };
294
+ dismissEl.style.setProperty(
295
+ "--gallerist-dismiss",
296
+ String(Math.min(1, Math.abs(dy) / closeDismiss))
297
+ );
298
+ schedule();
299
+ }
300
+ };
301
+ const onUp = (e) => {
302
+ if (!pointers.has(e.pointerId)) return;
303
+ pointers.delete(e.pointerId);
304
+ release(e.pointerId);
305
+ if (frame) {
306
+ caf(frame);
307
+ frame = 0;
308
+ }
309
+ if (intent === "zoom" && pointers.size === 1) {
310
+ live = settle(live);
311
+ commit(live);
312
+ const [remaining] = [...pointers.values()];
313
+ startZoom = { ...live };
314
+ start = remaining ? { x: remaining.x, y: remaining.y } : null;
315
+ intent = "none";
316
+ return;
317
+ }
318
+ const dx = start ? e.clientX - start.x : 0;
319
+ const dy = start ? e.clientY - start.y : 0;
320
+ if (intent === "none") intent = resolveIntent(dx, dy);
321
+ switch (intent) {
322
+ case "pan":
323
+ commit(settle(live));
324
+ break;
325
+ case "zoom":
326
+ if (pointers.size === 0) commit(settle(live));
327
+ break;
328
+ case "navigate":
329
+ if (Math.abs(dx) >= commitNav) dx < 0 ? nav.next() : nav.prev();
330
+ break;
331
+ case "close":
332
+ dismissEl.style.removeProperty("--gallerist-dismiss");
333
+ if (Math.abs(dy) >= closeDismiss) viewer.close();
334
+ else commit(startZoom);
335
+ break;
336
+ case "secondary":
337
+ if (-dy >= commitNav) nav.nextItem();
338
+ break;
339
+ }
340
+ if (pointers.size === 0) {
341
+ start = null;
342
+ intent = "none";
343
+ gestureGeom = null;
344
+ }
345
+ };
346
+ const onCancel = (e) => {
347
+ if (!pointers.has(e.pointerId)) return;
348
+ for (const id of pointers.keys()) release(id);
349
+ pointers.clear();
350
+ if (frame) {
351
+ caf(frame);
352
+ frame = 0;
353
+ }
354
+ dismissEl.style.removeProperty("--gallerist-dismiss");
355
+ const content = getContent();
356
+ if (content) content.style.transform = zoomTransform(viewer.getState().zoom);
357
+ start = null;
358
+ intent = "none";
359
+ gestureGeom = null;
360
+ };
361
+ const onWheel = (e) => {
362
+ e.preventDefault();
363
+ if (!wheelGeom) wheelGeom = getGeometry();
364
+ const g = wheelGeom;
365
+ const base = wheelZoom ?? viewer.getState().zoom;
366
+ const factor = e.deltaY < 0 ? 1.15 : 1 / 1.15;
367
+ const newScale = internal.clampScale(base.scale * factor, fitOf(g), maxScale);
368
+ const focal = { x: e.clientX - g.stageRect.left, y: e.clientY - g.stageRect.top };
369
+ wheelZoom = settle(internal.focalZoom(base, focal, newScale), g);
370
+ live = { ...wheelZoom };
371
+ schedule();
372
+ if (wheelTimer) clearTimeout(wheelTimer);
373
+ wheelTimer = setTimeout(commitWheel, WHEEL_COMMIT_MS);
374
+ };
375
+ const onDblClick = (e) => {
376
+ const hit = e.target;
377
+ if (hit?.closest?.("button, a[href], input, select, textarea, [data-interactive]")) return;
378
+ flushWheel();
379
+ const z = viewer.getState().zoom;
380
+ const target = internal.toggleZoom(z.scale, fit(), maxScale);
381
+ commit(settle(internal.focalZoom(z, toStage({ x: e.clientX, y: e.clientY }), target)));
382
+ };
383
+ const onDragStart = (e) => {
384
+ const target = e.target;
385
+ const content = getContent();
386
+ if (!target || !content?.contains(target)) return;
387
+ if (target.closest?.(".gallerist-overlays-content")) return;
388
+ e.preventDefault();
389
+ };
390
+ stage.addEventListener("dragstart", onDragStart);
391
+ stage.addEventListener("pointerdown", onDown);
392
+ stage.addEventListener("pointermove", onMove);
393
+ stage.addEventListener("pointerup", onUp);
394
+ stage.addEventListener("pointercancel", onCancel);
395
+ stage.addEventListener("wheel", onWheel, { passive: false });
396
+ stage.addEventListener("dblclick", onDblClick);
397
+ return () => {
398
+ stage.removeEventListener("dragstart", onDragStart);
399
+ stage.removeEventListener("pointerdown", onDown);
400
+ stage.removeEventListener("pointermove", onMove);
401
+ stage.removeEventListener("pointerup", onUp);
402
+ stage.removeEventListener("pointercancel", onCancel);
403
+ stage.removeEventListener("wheel", onWheel);
404
+ stage.removeEventListener("dblclick", onDblClick);
405
+ if (frame) caf(frame);
406
+ flushWheel();
407
+ };
408
+ }
409
+ function currentGroupHasMultiple(viewer) {
410
+ const col = viewer.getCollection();
411
+ const g = viewer.getState().cursor.g;
412
+ return (col.groups[g]?.items.length ?? 0) > 1;
413
+ }
414
+
415
+ // src/idle.ts
416
+ var ATTR = "data-gallerist-idle";
417
+ var ACTIVITY = ["pointermove", "pointerdown", "keydown", "wheel", "touchstart"];
418
+ var REARM_THROTTLE_MS = 200;
419
+ function prefersReducedMotion(root) {
420
+ const win = root.ownerDocument?.defaultView;
421
+ return win?.matchMedia?.("(prefers-reduced-motion: reduce)").matches ?? false;
422
+ }
423
+ function createIdleController(root, opts = {}) {
424
+ const delay = opts.delay ?? 2500;
425
+ const reduced = opts.reducedMotion ?? prefersReducedMotion(root);
426
+ if (reduced) {
427
+ return { poke() {
428
+ }, destroy() {
429
+ } };
430
+ }
431
+ let timer = null;
432
+ let alive = true;
433
+ let lastArm = 0;
434
+ let isIdle = false;
435
+ const wake = () => {
436
+ if (!isIdle) return;
437
+ isIdle = false;
438
+ root.removeAttribute(ATTR);
439
+ };
440
+ const goIdle = () => {
441
+ if (!alive || isIdle) return;
442
+ isIdle = true;
443
+ root.setAttribute(ATTR, "");
444
+ };
445
+ const arm = () => {
446
+ if (timer != null) clearTimeout(timer);
447
+ timer = setTimeout(goIdle, delay);
448
+ lastArm = Date.now();
449
+ };
450
+ const poke = () => {
451
+ wake();
452
+ arm();
453
+ };
454
+ const onActivity = () => {
455
+ wake();
456
+ if (Date.now() - lastArm >= REARM_THROTTLE_MS) arm();
457
+ };
458
+ for (const ev of ACTIVITY) root.addEventListener(ev, onActivity, { passive: true });
459
+ arm();
460
+ return {
461
+ poke,
462
+ destroy() {
463
+ alive = false;
464
+ if (timer != null) clearTimeout(timer);
465
+ for (const ev of ACTIVITY) root.removeEventListener(ev, onActivity);
466
+ root.removeAttribute(ATTR);
467
+ }
468
+ };
469
+ }
470
+
471
+ // src/overlays/position.ts
472
+ function containRect(container, natural) {
473
+ const s = Math.min(container.w / natural.w, container.h / natural.h);
474
+ const w = natural.w * s;
475
+ const h = natural.h * s;
476
+ return { x: (container.w - w) / 2, y: (container.h - h) / 2, w, h };
477
+ }
478
+ function contentAnchorBox(anchor, stage, natural, rect = containRect(stage, natural)) {
479
+ const s = rect.w / natural.w;
480
+ const space = anchor.space ?? "normalized";
481
+ const mapX = (x) => space === "natural" ? rect.x + x * s : rect.x + x * rect.w;
482
+ const mapY = (y) => space === "natural" ? rect.y + y * s : rect.y + y * rect.h;
483
+ const mapW = (w) => space === "natural" ? w * s : w * rect.w;
484
+ const mapH = (h) => space === "natural" ? h * s : h * rect.h;
485
+ if (anchor.kind === "point") {
486
+ return { left: mapX(anchor.x), top: mapY(anchor.y), width: null, height: null };
487
+ }
488
+ return {
489
+ left: mapX(anchor.x),
490
+ top: mapY(anchor.y),
491
+ width: mapW(anchor.w),
492
+ height: mapH(anchor.h)
493
+ };
494
+ }
495
+ function resolveScaleWithImage(anchor, override) {
496
+ if (override !== void 0) return override;
497
+ return anchor.kind === "region";
498
+ }
499
+ function counterScaleTransform(zoomScale) {
500
+ return `translate(-50%, -50%) scale(${1 / zoomScale})`;
501
+ }
502
+ function viewportAnchorStyle(at, offset = [0, 0]) {
503
+ const [ox, oy] = offset;
504
+ const style = { position: "absolute" };
505
+ if (at.startsWith("top")) style.top = `${oy}px`;
506
+ else if (at.startsWith("bottom")) style.bottom = `${oy}px`;
507
+ else style.top = "50%";
508
+ if (at.endsWith("left")) style.left = `${ox}px`;
509
+ else if (at.endsWith("right")) style.right = `${ox}px`;
510
+ else style.left = "50%";
511
+ const tx = at === "top" || at === "bottom" || at === "center" ? "-50%" : "0";
512
+ const ty = at === "left" || at === "right" || at === "center" ? "-50%" : "0";
513
+ if (tx !== "0" || ty !== "0") style.transform = `translate(${tx}, ${ty})`;
514
+ return style;
515
+ }
516
+ function orderByLayer(specs) {
517
+ return [...specs].sort((a, b) => (a.layer ?? 0) - (b.layer ?? 0));
518
+ }
519
+ var FOCUSABLE = 'a[href], button, input, select, textarea, [tabindex]:not([tabindex="-1"])';
520
+ function gatherOverlays(group, item, globals) {
521
+ return [...globals, ...group.overlays ?? [], ...item.overlays ?? []];
522
+ }
523
+ function updateOverlays(entries, zoomScale) {
524
+ for (const { el: el2, spec, counterScaled } of entries) {
525
+ const visible = !spec.visibleAt || zoomScale >= spec.visibleAt[0] && zoomScale <= spec.visibleAt[1];
526
+ el2.style.display = visible ? "" : "none";
527
+ if (counterScaled) el2.style.transform = counterScaleTransform(zoomScale);
528
+ }
529
+ }
530
+ function paintOverlays(viewportHost, contentHost, specs, ctx) {
531
+ const active = document.activeElement;
532
+ const focusedId = active && (viewportHost.contains(active) || contentHost.contains(active)) ? active.closest("[data-overlay]")?.dataset.overlay : void 0;
533
+ viewportHost.replaceChildren();
534
+ contentHost.replaceChildren();
535
+ const entries = [];
536
+ for (const spec of orderByLayer(specs)) {
537
+ const host = document.createElement("div");
538
+ host.className = spec.class ? `gallerist-overlay ${spec.class}` : "gallerist-overlay";
539
+ host.dataset.overlay = spec.id;
540
+ host.style.position = "absolute";
541
+ if (spec.layer !== void 0) host.style.zIndex = String(spec.layer);
542
+ host.style.pointerEvents = spec.interactive ? "auto" : "none";
543
+ const visible = !spec.visibleAt || ctx.zoom.scale >= spec.visibleAt[0] && ctx.zoom.scale <= spec.visibleAt[1];
544
+ if (!visible) host.style.display = "none";
545
+ if (spec.interactive) {
546
+ host.dataset.interactive = "";
547
+ host.addEventListener(
548
+ "click",
549
+ () => internal.getInternals(ctx.viewer).emit("overlayClick", {
550
+ viewer: ctx.viewer,
551
+ cursor: ctx.viewer.getState().cursor,
552
+ group: ctx.group,
553
+ item: ctx.item,
554
+ id: spec.id
555
+ })
556
+ );
557
+ } else {
558
+ host.setAttribute("aria-hidden", "true");
559
+ }
560
+ const out = spec.render({
561
+ group: ctx.group,
562
+ item: ctx.item,
563
+ zoom: ctx.zoom,
564
+ viewer: ctx.viewer
565
+ });
566
+ if (typeof out === "string") host.textContent = out;
567
+ else host.appendChild(out);
568
+ if (spec.interactive && !host.querySelector(FOCUSABLE)) host.tabIndex = 0;
569
+ let counterScaled = false;
570
+ if (spec.anchor.kind === "viewport") {
571
+ Object.assign(host.style, viewportAnchorStyle(spec.anchor.at, spec.anchor.offset));
572
+ viewportHost.appendChild(host);
573
+ } else {
574
+ const box = contentAnchorBox(spec.anchor, ctx.stage, ctx.natural, ctx.box);
575
+ host.style.left = `${box.left}px`;
576
+ host.style.top = `${box.top}px`;
577
+ if (box.width !== null) host.style.width = `${box.width}px`;
578
+ if (box.height !== null) host.style.height = `${box.height}px`;
579
+ counterScaled = !resolveScaleWithImage(spec.anchor, spec.scaleWithImage);
580
+ if (counterScaled) host.style.transform = counterScaleTransform(ctx.zoom.scale);
581
+ contentHost.appendChild(host);
582
+ }
583
+ entries.push({ el: host, spec, counterScaled });
584
+ }
585
+ if (focusedId) {
586
+ const host = entries.find((e) => e.spec.id === focusedId)?.el;
587
+ const target = host?.querySelector(FOCUSABLE) ?? (host?.tabIndex === 0 ? host : null);
588
+ target?.focus({ preventScroll: true });
589
+ }
590
+ return entries;
591
+ }
592
+
593
+ // src/presets.ts
594
+ var PRESETS = {
595
+ // classic lightbox: bottom rail, side arrows, persistent chrome.
596
+ filmstrip: {
597
+ rootAttrs: { "data-gallerist-preset": "filmstrip" },
598
+ arrows: "pill-right",
599
+ autoHide: false,
600
+ rail: "bottom",
601
+ thumbnails: { itemAxis: "vertical" },
602
+ navigator: false
603
+ },
604
+ // full-bleed: rail hidden by default, chrome auto-hides on idle, minimal UI.
605
+ immersive: {
606
+ rootAttrs: { "data-gallerist-preset": "immersive" },
607
+ arrows: "pill-right",
608
+ autoHide: true,
609
+ rail: "hidden",
610
+ thumbnails: { itemAxis: "vertical" },
611
+ navigator: false
612
+ },
613
+ // time-series reviewer: a bottom timeline rail (a time over each group, from group.meta.time),
614
+ // numbered items, the zoom navigator, and no control over the media (review needs every pixel):
615
+ // docked arrows, the item navigator on the item column or the caption row.
616
+ reviewer: {
617
+ rootAttrs: { "data-gallerist-preset": "reviewer" },
618
+ arrows: "docked",
619
+ autoHide: false,
620
+ rail: "bottom",
621
+ thumbnails: { itemAxis: "vertical", timeline: true, numberItems: true },
622
+ navigator: true
623
+ },
624
+ // a gallery wall: the work framed with room around it, a placard beside it (caption + item
625
+ // navigator), numbered items inline in a bottom rail. Pairs with `skin: 'editorial'`.
626
+ editorial: {
627
+ rootAttrs: { "data-gallerist-preset": "editorial" },
628
+ arrows: "pill-right",
629
+ autoHide: false,
630
+ rail: "bottom",
631
+ thumbnails: { itemAxis: "horizontal", numberItems: true },
632
+ frame: true,
633
+ itemIndicator: "placard"
634
+ },
635
+ // a walkthrough of named sections: a section index down the side (names, counts, the active section
636
+ // expanded), ←/→ between sections on labelled arrows, ↑/↓ within one, and framed media so nothing
637
+ // covers the photo. Pairs with `grid({ layout: 'sections', startOpen: true })`.
638
+ tour: {
639
+ rootAttrs: { "data-gallerist-preset": "tour" },
640
+ arrows: "pill-right",
641
+ autoHide: false,
642
+ rail: "left",
643
+ thumbnails: { itemAxis: "horizontal", groupDisplay: "index", numberItems: true },
644
+ frame: true,
645
+ itemIndicator: "dots",
646
+ groupAxis: "horizontal",
647
+ arrowLabels: true
648
+ },
649
+ // browse-heavy: a vertical rail down the right edge (pairs well with the grid plugin).
650
+ contact: {
651
+ rootAttrs: { "data-gallerist-preset": "contact" },
652
+ arrows: "pill-right",
653
+ autoHide: false,
654
+ rail: "right",
655
+ thumbnails: { itemAxis: "vertical" },
656
+ navigator: false
657
+ }
658
+ };
659
+ for (const p of Object.values(PRESETS))
660
+ for (const o of [p.rootAttrs, p.thumbnails, p]) Object.freeze(o);
661
+ function resolvePreset(name) {
662
+ const preset = PRESETS[name];
663
+ if (!preset && typeof console !== "undefined") {
664
+ console.warn(`[gallerist] unknown preset "${name}" \u2014 falling back to "filmstrip"`);
665
+ }
666
+ return preset ?? PRESETS.filmstrip;
667
+ }
668
+
669
+ // src/regions.ts
670
+ var ADDITIVE = /* @__PURE__ */ new Set(["toolbar", "footer", "aside"]);
671
+ function createRegionMounter(hosts, ctx) {
672
+ const rendered = /* @__PURE__ */ new WeakMap();
673
+ const applied = /* @__PURE__ */ new Map();
674
+ return (plugins) => {
675
+ const desired = /* @__PURE__ */ new Map();
676
+ for (const host of Object.values(hosts)) desired.set(host, []);
677
+ const claimedBy = /* @__PURE__ */ new Map();
678
+ for (const plugin of plugins) {
679
+ if (!plugin.regions) continue;
680
+ let nodes = rendered.get(plugin);
681
+ if (!nodes) {
682
+ nodes = /* @__PURE__ */ new Map();
683
+ rendered.set(plugin, nodes);
684
+ }
685
+ for (const [name, render] of Object.entries(plugin.regions)) {
686
+ if (!render) continue;
687
+ const host = hosts[name];
688
+ if (!host) {
689
+ console.warn(
690
+ `[gallerist] plugin "${plugin.name}" targets region "${name}", which the default renderer does not host`
691
+ );
692
+ continue;
693
+ }
694
+ let node = nodes.get(name);
695
+ if (!node) {
696
+ node = render(ctx);
697
+ nodes.set(name, node);
698
+ }
699
+ const list = desired.get(host) ?? [];
700
+ if (!ADDITIVE.has(name) && claimedBy.has(name)) {
701
+ console.warn(
702
+ `[gallerist] region "${name}" claimed by multiple plugins; "${plugin.name}" wins over "${claimedBy.get(name)}"`
703
+ );
704
+ list.length = 0;
705
+ }
706
+ list.push(node);
707
+ desired.set(host, list);
708
+ if (!ADDITIVE.has(name)) claimedBy.set(name, plugin.name);
709
+ }
710
+ }
711
+ for (const [host, list] of desired) {
712
+ const prev = applied.get(host) ?? [];
713
+ if (prev.length === list.length && prev.every((n, i) => n === list[i])) continue;
714
+ reconcile(host, prev, list);
715
+ applied.set(host, list);
716
+ }
717
+ };
718
+ }
719
+ function reconcile(host, prev, next) {
720
+ const keep = new Set(next);
721
+ for (const node of prev) if (!keep.has(node) && node.parentNode === host) host.removeChild(node);
722
+ let before = null;
723
+ for (const node of next) {
724
+ if (before) {
725
+ if (before.nextSibling !== node) host.insertBefore(node, before.nextSibling);
726
+ } else if (node.parentNode !== host) {
727
+ host.appendChild(node);
728
+ }
729
+ before = node;
730
+ }
731
+ }
732
+ function aggregateKeymap(plugins) {
733
+ const map = {};
734
+ for (const plugin of plugins) {
735
+ if (plugin.keymap) Object.assign(map, plugin.keymap);
736
+ }
737
+ return map;
738
+ }
739
+
740
+ // src/strings.ts
741
+ var DEFAULT_STRINGS = {
742
+ dialog: "Media viewer",
743
+ carousel: "carousel",
744
+ close: "Close",
745
+ previous: "Previous",
746
+ next: "Next",
747
+ previousItem: "Previous item",
748
+ nextItem: "Next item",
749
+ previousTo: (name) => `Previous: ${name}`,
750
+ nextTo: (name) => `Next: ${name}`,
751
+ resetZoom: "Reset zoom",
752
+ showThumbnails: "Show thumbnails",
753
+ hideThumbnails: "Hide thumbnails",
754
+ caption: "Caption",
755
+ item: (n) => `Item ${n}`,
756
+ groupOf: (n, total) => `Group ${n} of ${total}`,
757
+ itemOf: (n, total) => `Item ${n} of ${total}`
758
+ };
759
+
760
+ // src/mount.ts
761
+ var sourceUrl = (source) => typeof source === "string" ? source : source && "src" in source ? source.src : null;
762
+ var str = (x) => typeof x === "string" ? x : "";
763
+ var mounts = 0;
764
+ function el(tag, className, attrs) {
765
+ const node = document.createElement(tag);
766
+ if (className) node.className = className;
767
+ if (attrs) for (const [k, v] of Object.entries(attrs)) node.setAttribute(k, v);
768
+ return node;
769
+ }
770
+ var FOCUSABLE_SELECTOR = 'a[href],area[href],button:not([disabled]),input:not([disabled]),select:not([disabled]),textarea:not([disabled]),iframe,[tabindex],[contenteditable="true"]';
771
+ function focusableWithin(root) {
772
+ return [...root.querySelectorAll(FOCUSABLE_SELECTOR)].filter(
773
+ // a hidden control can't take focus: counted as the last one, it would let Tab out of the trap
774
+ (node) => node.tabIndex !== -1 && !node.hasAttribute("disabled") && node.checkVisibility?.() !== false
775
+ );
776
+ }
777
+ function isEditableTarget(target) {
778
+ const node = target;
779
+ return !!node && (node.isContentEditable || /^(input|textarea|select)$/i.test(node.tagName ?? ""));
780
+ }
781
+ function animationMs(style) {
782
+ const ms = (v) => {
783
+ const n = Number.parseFloat(v);
784
+ if (!Number.isFinite(n)) return 0;
785
+ return v.trim().endsWith("ms") ? n : n * 1e3;
786
+ };
787
+ const count = (v) => {
788
+ const n = Number.parseFloat(v ?? "1");
789
+ return Number.isFinite(n) && n > 0 ? n : 1;
790
+ };
791
+ const at = (list, i) => list[i % list.length];
792
+ const durations = style.animationDuration.split(",");
793
+ const delays = style.animationDelay.split(",");
794
+ const iterations = (style.animationIterationCount || "1").split(",");
795
+ return Math.max(
796
+ 0,
797
+ ...durations.map((d, i) => ms(at(delays, i) ?? "0s") + ms(d) * count(at(iterations, i)))
798
+ );
799
+ }
800
+ var MAX_ZOOM = 4;
801
+ function applyTransform(content, zoom) {
802
+ content.style.transform = zoomTransform(zoom);
803
+ }
804
+ var savedBodyOverflow = "";
805
+ var anyOverlayMounted = () => !!document.querySelector('.gallerist[data-gallerist-mode="overlay"]');
806
+ function lockBodyScroll() {
807
+ if (!anyOverlayMounted()) savedBodyOverflow = document.body.style.overflow;
808
+ document.body.style.overflow = "hidden";
809
+ }
810
+ function unlockBodyScroll() {
811
+ if (!anyOverlayMounted()) document.body.style.overflow = savedBodyOverflow;
812
+ }
813
+ var inertedByGallerist = /* @__PURE__ */ new Set();
814
+ function inertBackground(root) {
815
+ for (const child of [...document.body.children]) {
816
+ if (child !== root && child instanceof HTMLElement && !child.hasAttribute("inert")) {
817
+ child.setAttribute("inert", "");
818
+ inertedByGallerist.add(child);
819
+ }
820
+ }
821
+ }
822
+ function releaseBackgroundInert() {
823
+ if (anyOverlayMounted()) return;
824
+ for (const el2 of inertedByGallerist) el2.removeAttribute("inert");
825
+ inertedByGallerist.clear();
826
+ }
827
+ function mount(viewer, opts = {}) {
828
+ const overlay = !opts.container;
829
+ const container = opts.container ?? null;
830
+ const controls = opts.controls ?? true;
831
+ const preload = opts.preload ?? { ahead: 2, behind: 1 };
832
+ const preset = opts.preset ? resolvePreset(opts.preset) : void 0;
833
+ const arrows = opts.arrows ?? preset?.arrows ?? "pill-right";
834
+ const autoHide = opts.autoHide ?? preset?.autoHide ?? true;
835
+ const showNavigator = opts.navigator ?? preset?.navigator ?? false;
836
+ const frame = opts.frame ?? preset?.frame ?? false;
837
+ const rail = opts.rail ?? preset?.rail ?? "bottom";
838
+ const railHidden = rail === "hidden";
839
+ const railPos = rail === "hidden" ? "bottom" : rail;
840
+ const orientation = rail === "left" || rail === "right" ? "vertical" : "horizontal";
841
+ const groupAxis = opts.groupAxis ?? preset?.groupAxis ?? orientation;
842
+ const vertical = groupAxis === "vertical";
843
+ let linear = false;
844
+ const arrowLabelsOpt = opts.arrowLabels ?? preset?.arrowLabels ?? false;
845
+ const arrowLabel = !arrowLabelsOpt || !controls || vertical || arrows !== "pill-right" ? null : arrowLabelsOpt === true ? (group) => str(group.caption) : arrowLabelsOpt;
846
+ const loop = opts.loop ?? viewer.loop;
847
+ const ui = { ...DEFAULT_STRINGS, ...opts.strings };
848
+ const itemIndicator = opts.itemIndicator ?? preset?.itemIndicator ?? "pills";
849
+ const placard = itemIndicator === "placard";
850
+ const internals = internal.getInternals(viewer);
851
+ const plugins = internals.getPlugins();
852
+ const baseAdapters = opts.adapters ?? [imageAdapter()];
853
+ const currentAdapters = () => [
854
+ ...baseAdapters,
855
+ ...internals.getPlugins().flatMap((p) => p.mediaAdapters ?? [])
856
+ ];
857
+ const currentGlobalOverlays = (group, item) => [
858
+ ...opts.overlays ?? [],
859
+ ...internals.getPlugins().flatMap(
860
+ (p) => typeof p.overlays === "function" ? p.overlays({ group, item }) : p.overlays ?? []
861
+ )
862
+ ];
863
+ let pluginKeymap = aggregateKeymap(plugins);
864
+ const pickAdapter = (type, item) => currentAdapters().find((a) => a.match ? a.match(item) : a.type === type);
865
+ let flatSeq = [];
866
+ let flatIdx = /* @__PURE__ */ new Map();
867
+ let flatCol = null;
868
+ const ensureFlat = () => {
869
+ const col = viewer.getCollection();
870
+ if (col === flatCol) return;
871
+ flatCol = col;
872
+ flatSeq = [];
873
+ flatIdx = /* @__PURE__ */ new Map();
874
+ col.groups.forEach(
875
+ (grp, g) => grp.items.forEach((_, i) => {
876
+ flatIdx.set(`${g}:${i}`, flatSeq.length);
877
+ flatSeq.push({ g, i });
878
+ })
879
+ );
880
+ };
881
+ const flatPosition = (g, i) => {
882
+ ensureFlat();
883
+ return flatIdx.get(`${g}:${i}`) ?? 0;
884
+ };
885
+ const linearStep = (dir) => {
886
+ ensureFlat();
887
+ if (flatSeq.length === 0) return;
888
+ const { g, i } = viewer.getState().cursor;
889
+ let n = (flatIdx.get(`${g}:${i}`) ?? 0) + dir;
890
+ if (n >= flatSeq.length) n = loop ? 0 : flatSeq.length - 1;
891
+ if (n < 0) n = loop ? flatSeq.length - 1 : 0;
892
+ const t = flatSeq[n];
893
+ if (t) viewer.goTo(t.g, t.i);
894
+ };
895
+ const nav = {
896
+ next: () => linear ? linearStep(1) : viewer.next(),
897
+ prev: () => linear ? linearStep(-1) : viewer.prev(),
898
+ nextItem: () => {
899
+ if (!linear) viewer.nextItem();
900
+ },
901
+ prevItem: () => {
902
+ if (!linear) viewer.prevItem();
903
+ }
904
+ };
905
+ const root = el("div", "gallerist");
906
+ root.setAttribute("data-gallerist-mode", overlay ? "overlay" : "inline");
907
+ root.setAttribute("data-gallerist-arrows", arrows);
908
+ root.setAttribute("data-gallerist-rail-pos", railPos);
909
+ root.setAttribute("data-gallerist-orientation", orientation);
910
+ root.setAttribute("data-gallerist-axis", groupAxis);
911
+ root.setAttribute("data-gallerist-rail", railHidden ? "collapsed" : "expanded");
912
+ if (frame) root.setAttribute("data-gallerist-frame", "");
913
+ if (placard) root.setAttribute("data-gallerist-indicator", "placard");
914
+ applyAppearance(root, { skin: opts.skin, accent: opts.accent, scheme: opts.scheme });
915
+ if (preset) for (const [k, val] of Object.entries(preset.rootAttrs)) root.setAttribute(k, val);
916
+ if (overlay) {
917
+ root.setAttribute("role", "dialog");
918
+ root.setAttribute("aria-modal", "true");
919
+ root.setAttribute("aria-label", opts.label ?? ui.dialog);
920
+ root.tabIndex = -1;
921
+ } else {
922
+ root.setAttribute("role", "group");
923
+ root.setAttribute("aria-roledescription", ui.carousel);
924
+ root.tabIndex = -1;
925
+ }
926
+ if (overlay) root.appendChild(el("div", "gallerist-backdrop"));
927
+ const bar = el("div", "gallerist-bar");
928
+ const main = el("div", "gallerist-main");
929
+ const stage = el("div", "gallerist-stage");
930
+ const track = el("div", "gallerist-track");
931
+ const viewportLayer = el("div", "gallerist-viewport-layer");
932
+ const stageRegion = el("div", "gallerist-region gallerist-region-stage");
933
+ stage.append(track, viewportLayer, stageRegion);
934
+ main.appendChild(stage);
935
+ const regionHosts = {
936
+ backdrop: el("div", "gallerist-region gallerist-region-backdrop"),
937
+ stage: stageRegion,
938
+ toolbar: el("div", "gallerist-region gallerist-region-toolbar"),
939
+ aside: el("div", "gallerist-region gallerist-region-aside"),
940
+ thumbnails: el("div", "gallerist-region gallerist-region-thumbnails"),
941
+ footer: el("div", "gallerist-region gallerist-region-footer"),
942
+ caption: el("div", "gallerist-region gallerist-region-caption")
943
+ };
944
+ const caption = el("div", "gallerist-caption");
945
+ const cid = `gallerist-caption-${++mounts}`;
946
+ const captionText = el("span", "gallerist-caption-text", { id: cid });
947
+ regionHosts.caption.id = `${cid}r`;
948
+ const capLabel = el("span", "", { id: `${cid}l`, hidden: "" });
949
+ capLabel.textContent = ui.caption;
950
+ const capScroll = placard ? regionHosts.caption : caption;
951
+ const fitCaption = () => {
952
+ const on = regionHosts.caption.hasChildNodes() && capScroll.scrollHeight > capScroll.clientHeight + 1;
953
+ if (!on && document.activeElement === capScroll) root.focus({ preventScroll: true });
954
+ const set = on ? "setAttribute" : "removeAttribute";
955
+ capScroll[set]("tabindex", "0");
956
+ capScroll[set]("role", "region");
957
+ capScroll[set]("aria-labelledby", capLabel.id);
958
+ };
959
+ const capObserver = typeof ResizeObserver !== "undefined" ? new ResizeObserver(fitCaption) : null;
960
+ caption.append(captionText, regionHosts.caption);
961
+ caption.hidden = true;
962
+ let anyCaption = false;
963
+ const syncCaption = () => {
964
+ const own = regionHosts.caption.hasChildNodes();
965
+ const { g, i } = viewer.getState().cursor;
966
+ captionText.textContent = own ? "" : str(viewer.getCollection().groups[g]?.items[i]?.caption);
967
+ const hide = !(own || controls && (anyCaption || placard));
968
+ caption.hidden = hide;
969
+ caption.toggleAttribute("data-gallerist-custom", own);
970
+ root[hide ? "removeAttribute" : "setAttribute"]("aria-describedby", `${cid} ${cid}r`);
971
+ for (const n of [capScroll, ...capScroll.children]) capObserver?.observe(n);
972
+ if (!own) fitCaption();
973
+ };
974
+ root.append(
975
+ regionHosts.backdrop,
976
+ bar,
977
+ main,
978
+ regionHosts.aside,
979
+ caption,
980
+ regionHosts.footer,
981
+ regionHosts.thumbnails,
982
+ capLabel
983
+ );
984
+ const live = el("div", "gallerist-live", { "aria-live": "polite", role: "status" });
985
+ root.appendChild(live);
986
+ let railCollapsed = railHidden;
987
+ let preFsCollapsed = null;
988
+ const railHandle = el("div", "gallerist-rail-handle");
989
+ const railToggle = el("button", "gallerist-rail-toggle", {
990
+ type: "button",
991
+ "aria-label": railHidden ? ui.showThumbnails : ui.hideThumbnails,
992
+ "aria-pressed": String(railHidden)
993
+ });
994
+ railToggle.innerHTML = internal.ICONS.collapse;
995
+ railHandle.appendChild(railToggle);
996
+ const setRailCollapsed = (v, manual = false) => {
997
+ railCollapsed = v;
998
+ root.setAttribute("data-gallerist-rail", v ? "collapsed" : "expanded");
999
+ railToggle.setAttribute("aria-pressed", String(v));
1000
+ railToggle.setAttribute("aria-label", v ? ui.showThumbnails : ui.hideThumbnails);
1001
+ if (manual) preFsCollapsed = null;
1002
+ };
1003
+ railToggle.addEventListener("click", () => setRailCollapsed(!railCollapsed, true));
1004
+ let counter;
1005
+ let title;
1006
+ let position;
1007
+ let itemNav;
1008
+ let controlsBox;
1009
+ let reset;
1010
+ let navigatorEl;
1011
+ let navigatorView;
1012
+ let prevArrow;
1013
+ let nextArrow;
1014
+ let posG = -1;
1015
+ let posM = -1;
1016
+ if (controls) {
1017
+ counter = el("div", "gallerist-counter");
1018
+ title = el("div", "gallerist-title");
1019
+ title.hidden = true;
1020
+ reset = el("button", "gallerist-zoom-reset", { type: "button", "aria-label": ui.resetZoom });
1021
+ reset.innerHTML = internal.ICONS.zoomReset;
1022
+ reset.hidden = true;
1023
+ reset.addEventListener("click", () => viewer.resetZoom());
1024
+ const close = el("button", "gallerist-close", { type: "button", "aria-label": ui.close });
1025
+ close.innerHTML = internal.ICONS.close;
1026
+ close.addEventListener("click", () => viewer.close());
1027
+ bar.append(counter, title, regionHosts.toolbar, reset, close);
1028
+ const prev = el("button", "gallerist-prev", { type: "button", "aria-label": ui.previous });
1029
+ prev.innerHTML = vertical ? internal.ICONS.itemPrev : internal.ICONS.prev;
1030
+ prev.addEventListener("click", () => nav.prev());
1031
+ const next = el("button", "gallerist-next", { type: "button", "aria-label": ui.next });
1032
+ next.innerHTML = vertical ? internal.ICONS.itemNext : internal.ICONS.next;
1033
+ next.addEventListener("click", () => nav.next());
1034
+ prevArrow = prev;
1035
+ nextArrow = next;
1036
+ const prevItem = el("button", "gallerist-item-prev", {
1037
+ type: "button",
1038
+ "aria-label": ui.previousItem
1039
+ });
1040
+ prevItem.innerHTML = vertical ? internal.ICONS.prev : internal.ICONS.itemPrev;
1041
+ prevItem.addEventListener("click", () => nav.prevItem());
1042
+ const nextItem = el("button", "gallerist-item-next", {
1043
+ type: "button",
1044
+ "aria-label": ui.nextItem
1045
+ });
1046
+ nextItem.innerHTML = vertical ? internal.ICONS.next : internal.ICONS.itemNext;
1047
+ nextItem.addEventListener("click", () => nav.nextItem());
1048
+ position = el("div", "gallerist-position");
1049
+ itemNav = el("div", "gallerist-itemnav");
1050
+ itemNav.hidden = true;
1051
+ itemNav.append(prevItem, position, nextItem);
1052
+ controlsBox = el("div", "gallerist-controls");
1053
+ controlsBox.append(prev, next);
1054
+ if (placard) caption.appendChild(itemNav);
1055
+ else if (arrows === "docked") main.after(caption, itemNav);
1056
+ else controlsBox.appendChild(itemNav);
1057
+ if (showNavigator) {
1058
+ navigatorEl = el("div", "gallerist-navigator", { "aria-hidden": "true" });
1059
+ navigatorEl.hidden = true;
1060
+ navigatorView = el("div", "gallerist-navigator-view");
1061
+ navigatorEl.appendChild(navigatorView);
1062
+ navigatorEl.addEventListener("pointerdown", onNavigatorDown);
1063
+ controlsBox.appendChild(navigatorEl);
1064
+ }
1065
+ main.appendChild(controlsBox);
1066
+ } else {
1067
+ bar.appendChild(regionHosts.toolbar);
1068
+ }
1069
+ const mounted = /* @__PURE__ */ new Map();
1070
+ let lastCursor = null;
1071
+ let lastWindowCol = null;
1072
+ let lastWindowG = -1;
1073
+ let lastWindowI = -1;
1074
+ let indexCol = null;
1075
+ let idIndex = /* @__PURE__ */ new Map();
1076
+ const groupIndex = (id) => {
1077
+ const col = viewer.getCollection();
1078
+ if (col !== indexCol) {
1079
+ indexCol = col;
1080
+ idIndex = new Map(col.groups.map((g, i) => [g.id, i]));
1081
+ }
1082
+ return idIndex.get(id) ?? -1;
1083
+ };
1084
+ const tagAxis = (currentSlide, cur) => {
1085
+ if (lastCursor && (lastCursor.g !== cur.g || lastCursor.i !== cur.i)) {
1086
+ const axis = lastCursor.g !== cur.g ? "group" : "item";
1087
+ currentSlide.removeAttribute("data-gallerist-enter");
1088
+ void currentSlide.offsetWidth;
1089
+ currentSlide.setAttribute("data-gallerist-enter", axis);
1090
+ }
1091
+ lastCursor = { g: cur.g, i: cur.i };
1092
+ };
1093
+ const teardownSlide = (rec) => {
1094
+ rec.abort?.abort();
1095
+ rec.media?.destroy();
1096
+ rec.slide.remove();
1097
+ };
1098
+ let rendering = false;
1099
+ const renderWindowInner = () => {
1100
+ const state = viewer.getState();
1101
+ const col = viewer.getCollection();
1102
+ const groupCount = col.groups.length;
1103
+ if (groupCount === 0) return;
1104
+ const cur = state.cursor;
1105
+ if (col === lastWindowCol && cur.g === lastWindowG && cur.i === lastWindowI) {
1106
+ const rec = mounted.get(col.groups[cur.g]?.id ?? "");
1107
+ const it = col.groups[cur.g]?.items[cur.i];
1108
+ if (rec && it && rec.itemId === it.id && rec.overlayItem === it) {
1109
+ applyTransform(rec.content, state.zoom);
1110
+ if (rec.overlayEls) updateOverlays(rec.overlayEls, state.zoom.scale);
1111
+ return;
1112
+ }
1113
+ }
1114
+ lastWindowCol = col;
1115
+ lastWindowG = cur.g;
1116
+ lastWindowI = cur.i;
1117
+ const idxs = internal.windowIndices(cur.g, groupCount, preload.ahead, preload.behind, viewer.loop);
1118
+ const wantKeys = idxs.map((g) => col.groups[g]?.id ?? "");
1119
+ const { enter, exit } = internal.reconcileKeys([...mounted.keys()], wantKeys);
1120
+ for (const k of exit) {
1121
+ const rec = mounted.get(k);
1122
+ if (rec) teardownSlide(rec);
1123
+ mounted.delete(k);
1124
+ }
1125
+ for (const k of enter) {
1126
+ const slide = el("div", "gallerist-slide", { "data-group": k });
1127
+ const content = el("div", "gallerist-content");
1128
+ const overlayHost = el("div", "gallerist-overlays-content");
1129
+ content.appendChild(overlayHost);
1130
+ slide.appendChild(content);
1131
+ track.appendChild(slide);
1132
+ mounted.set(k, { slide, content, overlayHost });
1133
+ }
1134
+ for (const [k, rec] of mounted) {
1135
+ const g = groupIndex(k);
1136
+ if (g < 0) continue;
1137
+ const isCurrent = g === cur.g;
1138
+ const group = col.groups[g];
1139
+ if (!group) continue;
1140
+ const itemIndex = isCurrent ? cur.i : 0;
1141
+ const it = group.items[itemIndex];
1142
+ if (it && rec.itemId !== it.id) {
1143
+ rec.abort?.abort();
1144
+ rec.media?.destroy();
1145
+ const abort = new AbortController();
1146
+ rec.media = pickAdapter(it.type, it)?.mount(it, rec.content, {
1147
+ signal: abort.signal,
1148
+ priority: isCurrent ? "high" : "low"
1149
+ });
1150
+ rec.abort = abort;
1151
+ rec.itemId = it.id;
1152
+ rec.activated = false;
1153
+ rec.content.appendChild(rec.overlayHost);
1154
+ frameSlide(rec);
1155
+ const mountedId = it.id;
1156
+ const slideKey = k;
1157
+ const loadBase = { viewer, group, item: it };
1158
+ internals.emit("load", { ...loadBase, cursor: viewer.cursor, id: mountedId });
1159
+ rec.media?.ready?.then(() => {
1160
+ internals.emit("loaded", { ...loadBase, cursor: viewer.cursor, id: mountedId });
1161
+ const r = mounted.get(slideKey);
1162
+ if (!r || r.itemId !== mountedId) return;
1163
+ frameSlide(r);
1164
+ if (groupIndex(slideKey) !== viewer.getState().cursor.g) return;
1165
+ r.overlayItem = void 0;
1166
+ if (attached) reclampZoom();
1167
+ }).catch((error) => {
1168
+ if (abort.signal.aborted) {
1169
+ const remounted = [...mounted.values()].some((r) => r.itemId === mountedId);
1170
+ if (!remounted) internals.cancelLoad(mountedId);
1171
+ return;
1172
+ }
1173
+ internals.emit("loadError", {
1174
+ ...loadBase,
1175
+ cursor: viewer.cursor,
1176
+ id: mountedId,
1177
+ error
1178
+ });
1179
+ });
1180
+ }
1181
+ if (isCurrent && !rec.activated) {
1182
+ rec.media?.activate?.();
1183
+ rec.activated = true;
1184
+ } else if (!isCurrent && rec.activated) {
1185
+ rec.media?.deactivate?.();
1186
+ rec.activated = false;
1187
+ }
1188
+ if (isCurrent) {
1189
+ tagAxis(rec.slide, cur);
1190
+ rec.slide.setAttribute("data-gallerist-current", "");
1191
+ rec.content.setAttribute("data-gallerist-current", "");
1192
+ applyTransform(rec.content, state.zoom);
1193
+ if (it) {
1194
+ if (rec.overlayItem !== it) {
1195
+ const r = stage.getBoundingClientRect();
1196
+ const stageSize2 = { w: r.width, h: r.height };
1197
+ rec.overlayEls = paintOverlays(
1198
+ viewportLayer,
1199
+ rec.overlayHost,
1200
+ gatherOverlays(group, it, currentGlobalOverlays(group, it)),
1201
+ {
1202
+ group,
1203
+ item: it,
1204
+ viewer,
1205
+ zoom: state.zoom,
1206
+ stage: stageSize2,
1207
+ natural: rec.media?.naturalSize?.() ?? stageSize2,
1208
+ box: paintedBox(stageSize2, rec.media?.naturalSize?.())
1209
+ }
1210
+ );
1211
+ rec.overlayItem = it;
1212
+ } else if (rec.overlayEls) {
1213
+ updateOverlays(rec.overlayEls, state.zoom.scale);
1214
+ }
1215
+ }
1216
+ } else {
1217
+ rec.slide.removeAttribute("data-gallerist-current");
1218
+ rec.content.removeAttribute("data-gallerist-current");
1219
+ rec.overlayHost.replaceChildren();
1220
+ rec.overlayEls = void 0;
1221
+ rec.overlayItem = void 0;
1222
+ }
1223
+ }
1224
+ const items = col.groups[cur.g]?.items ?? [];
1225
+ const want = /* @__PURE__ */ new Map();
1226
+ for (let d = -preload.behind; d <= preload.ahead; d++) {
1227
+ const it = d ? items[cur.i + d] : void 0;
1228
+ if (it && sourceUrl(it.source) && pickAdapter(it.type, it)?.mount === mountImage)
1229
+ want.set(it.id, it);
1230
+ }
1231
+ for (const [id, img] of warm) if (!want.has(id)) unwarm(id, img);
1232
+ const run = ++warmRun;
1233
+ const start = () => {
1234
+ if (run !== warmRun) return;
1235
+ for (const [id, it] of want) {
1236
+ const s = it.source;
1237
+ if (!warm.has(id)) {
1238
+ warm.set(
1239
+ id,
1240
+ Object.assign(new Image(), {
1241
+ fetchPriority: "low",
1242
+ sizes: s.sizes ?? "",
1243
+ srcset: s.srcset ?? "",
1244
+ src: sourceUrl(it.source)
1245
+ })
1246
+ );
1247
+ }
1248
+ }
1249
+ };
1250
+ const ready = mounted.get(col.groups[cur.g]?.id ?? "")?.media?.ready;
1251
+ if (ready) ready.then(start, start);
1252
+ else start();
1253
+ };
1254
+ const warm = /* @__PURE__ */ new Map();
1255
+ let warmRun = 0;
1256
+ const unwarm = (id, img) => {
1257
+ img.srcset = img.src = "";
1258
+ warm.delete(id);
1259
+ };
1260
+ const renderWindow = () => {
1261
+ if (rendering) return;
1262
+ rendering = true;
1263
+ try {
1264
+ renderWindowInner();
1265
+ } finally {
1266
+ rendering = false;
1267
+ }
1268
+ };
1269
+ const PILL_MAX = 8;
1270
+ const DOT_MAX = 12;
1271
+ const updatePosition = (g, i, itemCount) => {
1272
+ if (!position) return;
1273
+ const mode = itemIndicator === "pills" && itemCount <= PILL_MAX ? "pills" : (itemIndicator === "dots" || placard) && itemCount <= DOT_MAX ? "dots" : "counter";
1274
+ if (g !== posG || itemCount !== posM || position.dataset.mode !== mode) {
1275
+ position.dataset.mode = mode;
1276
+ position.replaceChildren();
1277
+ const text2 = el("span", "gallerist-position-text");
1278
+ position.appendChild(text2);
1279
+ if (mode !== "counter") {
1280
+ for (let k = 0; k < itemCount; k++) {
1281
+ const jump = el("button", mode === "pills" ? "gallerist-pill" : "gallerist-dot", {
1282
+ type: "button",
1283
+ "aria-label": ui.item(k + 1)
1284
+ });
1285
+ if (mode === "pills") jump.textContent = String(k + 1);
1286
+ jump.addEventListener("click", () => viewer.goTo(g, k));
1287
+ position.appendChild(jump);
1288
+ }
1289
+ }
1290
+ posG = g;
1291
+ posM = itemCount;
1292
+ }
1293
+ const text = position.querySelector(".gallerist-position-text");
1294
+ if (text) text.textContent = `${i + 1} / ${itemCount}`;
1295
+ for (const [k, jump] of position.querySelectorAll(".gallerist-pill, .gallerist-dot").entries()) {
1296
+ if (k === i) jump.setAttribute("data-active", "");
1297
+ else jump.removeAttribute("data-active");
1298
+ }
1299
+ };
1300
+ const setHiddenKeepingFocus = (node, hidden) => {
1301
+ if (hidden && !node.hidden && node.contains(document.activeElement)) {
1302
+ root.focus({ preventScroll: true });
1303
+ }
1304
+ node.hidden = hidden;
1305
+ };
1306
+ const setArrowLabel = (arrow, name, plain, named) => {
1307
+ if (!arrow) return;
1308
+ let label = arrow.querySelector(".gallerist-arrow-label");
1309
+ if (!label) {
1310
+ label = el("span", "gallerist-arrow-label", { "aria-hidden": "true" });
1311
+ arrow.appendChild(label);
1312
+ }
1313
+ label.textContent = name ?? "";
1314
+ label.hidden = !name;
1315
+ arrow.setAttribute("aria-label", name ? named(name) : plain);
1316
+ };
1317
+ let chromeG = -1;
1318
+ let chromeI = -1;
1319
+ let chromeCol = null;
1320
+ const updateChrome = () => {
1321
+ const col = viewer.getCollection();
1322
+ const cur = viewer.getState().cursor;
1323
+ if (col === chromeCol && cur.g === chromeG && cur.i === chromeI) return;
1324
+ if (col !== chromeCol) {
1325
+ anyCaption = col.groups.some((grp) => grp.items.some((it) => str(it.caption)));
1326
+ root.toggleAttribute(
1327
+ "data-gallerist-multi",
1328
+ col.groups.some((grp) => grp.items.length > 1)
1329
+ );
1330
+ const one = col.groups.length < 2;
1331
+ if (one && arrows === "docked" && !linear && [prevArrow, nextArrow].includes(document.activeElement))
1332
+ root.focus({ preventScroll: true });
1333
+ root.toggleAttribute("data-gallerist-one-group", one);
1334
+ }
1335
+ chromeCol = col;
1336
+ chromeG = cur.g;
1337
+ chromeI = cur.i;
1338
+ const G = col.groups.length;
1339
+ const group = col.groups[cur.g];
1340
+ const item = group?.items[cur.i];
1341
+ const itemCount = group?.items.length ?? 0;
1342
+ const multi = !linear && itemCount > 1;
1343
+ if (itemNav) setHiddenKeepingFocus(itemNav, !multi);
1344
+ if (title) {
1345
+ title.textContent = str(group?.caption);
1346
+ title.hidden = !title.textContent;
1347
+ }
1348
+ if (arrowLabel) {
1349
+ const nameAt = (step) => {
1350
+ const n = viewer.loop && G > 1 ? (cur.g + step + G) % G : cur.g + step;
1351
+ const target = col.groups[n];
1352
+ return target && !linear ? arrowLabel(target, n) : void 0;
1353
+ };
1354
+ setArrowLabel(prevArrow, nameAt(-1), ui.previous, ui.previousTo);
1355
+ setArrowLabel(nextArrow, nameAt(1), ui.next, ui.nextTo);
1356
+ }
1357
+ syncCaption();
1358
+ const desc = item?.alt ? ` \u2014 ${item.alt}` : "";
1359
+ if (linear) {
1360
+ const pos = flatPosition(cur.g, cur.i) + 1;
1361
+ const total = flatSeq.length;
1362
+ if (counter) counter.textContent = `${pos} / ${total}`;
1363
+ live.textContent = `${ui.itemOf(pos, total)}${desc}`;
1364
+ } else {
1365
+ if (counter) counter.textContent = `${cur.g + 1} / ${G}`;
1366
+ if (multi) updatePosition(cur.g, cur.i, itemCount);
1367
+ live.textContent = multi ? `${ui.groupOf(cur.g + 1, G)}, ${ui.itemOf(cur.i + 1, itemCount)}${desc}` : `${ui.groupOf(cur.g + 1, G)}${desc}`;
1368
+ }
1369
+ };
1370
+ const updateZoomChrome = () => {
1371
+ if (reset) setHiddenKeepingFocus(reset, !(viewer.getState().zoom.scale > 1.001));
1372
+ updateNavigator();
1373
+ };
1374
+ function updateNavigator() {
1375
+ if (!navigatorEl || !navigatorView) return;
1376
+ const z = viewer.getState().zoom;
1377
+ const rec = currentRec();
1378
+ const natural = rec?.media?.naturalSize?.();
1379
+ const { g, i } = viewer.getState().cursor;
1380
+ const it = viewer.getCollection().groups[g]?.items[i];
1381
+ const src = it ? sourceUrl(it.source) ?? sourceUrl(it.thumb) : null;
1382
+ const size = stageSize();
1383
+ const zoomable = rec?.media?.supportsZoom !== false;
1384
+ if (!(z.scale > 1.001) || !zoomable || !natural || !src || !(size.w > 0)) {
1385
+ navigatorEl.hidden = true;
1386
+ return;
1387
+ }
1388
+ const box = mediaBox(size);
1389
+ const url = `url(${JSON.stringify(src)})`;
1390
+ if (navigatorEl.style.backgroundImage !== url) navigatorEl.style.backgroundImage = url;
1391
+ navigatorEl.style.aspectRatio = `${natural.w} / ${natural.h}`;
1392
+ navigatorEl.dataset.shape = natural.h > natural.w ? "portrait" : "landscape";
1393
+ const clamp01 = (n) => Math.min(1, Math.max(0, n));
1394
+ const x0 = clamp01((-z.x / z.scale - box.x) / box.w);
1395
+ const y0 = clamp01((-z.y / z.scale - box.y) / box.h);
1396
+ const x1 = clamp01(((size.w - z.x) / z.scale - box.x) / box.w);
1397
+ const y1 = clamp01(((size.h - z.y) / z.scale - box.y) / box.h);
1398
+ Object.assign(navigatorView.style, {
1399
+ left: `${x0 * 100}%`,
1400
+ top: `${y0 * 100}%`,
1401
+ width: `${(x1 - x0) * 100}%`,
1402
+ height: `${(y1 - y0) * 100}%`
1403
+ });
1404
+ navigatorEl.hidden = false;
1405
+ }
1406
+ const panNavigatorTo = (e) => {
1407
+ if (!navigatorEl) return;
1408
+ const r = navigatorEl.getBoundingClientRect();
1409
+ const size = stageSize();
1410
+ if (!(r.width > 0) || !(size.w > 0)) return;
1411
+ const box = mediaBox(size);
1412
+ const z = viewer.getState().zoom;
1413
+ const cx = box.x + (e.clientX - r.left) / r.width * box.w;
1414
+ const cy = box.y + (e.clientY - r.top) / r.height * box.h;
1415
+ internals.setZoom({
1416
+ scale: z.scale,
1417
+ x: size.w / 2 - cx * z.scale,
1418
+ y: size.h / 2 - cy * z.scale
1419
+ });
1420
+ };
1421
+ function onNavigatorDown(e) {
1422
+ if (e.button !== 0) return;
1423
+ const nav2 = e.currentTarget;
1424
+ e.preventDefault();
1425
+ e.stopPropagation();
1426
+ nav2.setPointerCapture?.(e.pointerId);
1427
+ panNavigatorTo(e);
1428
+ const move = (ev) => panNavigatorTo(ev);
1429
+ const up = () => {
1430
+ nav2.removeEventListener("pointermove", move);
1431
+ nav2.removeEventListener("pointerup", up);
1432
+ nav2.removeEventListener("pointercancel", up);
1433
+ nav2.removeEventListener("lostpointercapture", up);
1434
+ };
1435
+ nav2.addEventListener("pointermove", move);
1436
+ nav2.addEventListener("pointerup", up);
1437
+ nav2.addEventListener("pointercancel", up);
1438
+ nav2.addEventListener("lostpointercapture", up);
1439
+ }
1440
+ let attached = false;
1441
+ let restoreFocus = null;
1442
+ const idle = autoHide ? createIdleController(root) : null;
1443
+ const attach = () => {
1444
+ if (attached) return;
1445
+ attached = true;
1446
+ if (overlay) {
1447
+ restoreFocus = document.activeElement;
1448
+ lockBodyScroll();
1449
+ document.body.appendChild(root);
1450
+ inertBackground(root);
1451
+ root.focus();
1452
+ }
1453
+ setDir();
1454
+ readFrameInset();
1455
+ idle?.poke();
1456
+ };
1457
+ const detach = () => {
1458
+ if (!attached) return;
1459
+ attached = false;
1460
+ if (overlay) {
1461
+ root.remove();
1462
+ unlockBodyScroll();
1463
+ releaseBackgroundInert();
1464
+ const target = typeof opts.returnFocus === "function" ? opts.returnFocus() : opts.returnFocus ?? null;
1465
+ (target ?? restoreFocus)?.focus?.();
1466
+ }
1467
+ };
1468
+ if (!overlay && container) {
1469
+ container.appendChild(root);
1470
+ attached = true;
1471
+ idle?.poke();
1472
+ }
1473
+ const sync = () => {
1474
+ const phase = viewer.getState().phase;
1475
+ if (root.getAttribute("data-gallerist-phase") !== phase)
1476
+ root.setAttribute("data-gallerist-phase", phase);
1477
+ if (overlay) {
1478
+ if (phase !== "closed") attach();
1479
+ else detach();
1480
+ }
1481
+ if (attached) {
1482
+ if (!root.hasAttribute("data-gallerist-dir")) setDir();
1483
+ renderWindow();
1484
+ updateChrome();
1485
+ updateZoomChrome();
1486
+ }
1487
+ };
1488
+ const zoomAroundCenter = (factor) => viewer.zoomTo(viewer.zoom.scale * factor);
1489
+ const arrowNav = vertical ? {
1490
+ ArrowDown: nav.next,
1491
+ ArrowUp: nav.prev,
1492
+ ArrowRight: nav.nextItem,
1493
+ ArrowLeft: nav.prevItem
1494
+ } : {
1495
+ ArrowRight: nav.next,
1496
+ ArrowLeft: nav.prev,
1497
+ ArrowDown: nav.nextItem,
1498
+ ArrowUp: nav.prevItem
1499
+ };
1500
+ const isRtl = () => {
1501
+ const dir = root.closest("[dir]")?.getAttribute("dir");
1502
+ if (dir) return dir === "rtl";
1503
+ try {
1504
+ return getComputedStyle(root).direction === "rtl";
1505
+ } catch {
1506
+ return false;
1507
+ }
1508
+ };
1509
+ const setDir = () => root.setAttribute("data-gallerist-dir", isRtl() ? "rtl" : "ltr");
1510
+ const onKey = (e) => {
1511
+ if (isEditableTarget(e.target) && e.key !== "Escape") return;
1512
+ const by = e.target === capScroll && capScroll.hasAttribute("tabindex") ? { ArrowUp: -40, ArrowDown: 40, Home: -1e9, End: 1e9 }[e.key] : 0;
1513
+ if (by) {
1514
+ capScroll.scrollTop += by;
1515
+ e.preventDefault();
1516
+ return;
1517
+ }
1518
+ if (e.key === "Tab" && overlay) {
1519
+ const tabbables = focusableWithin(root);
1520
+ const first = tabbables[0];
1521
+ const last = tabbables[tabbables.length - 1];
1522
+ if (!first || !last) {
1523
+ e.preventDefault();
1524
+ return;
1525
+ }
1526
+ const active = document.activeElement;
1527
+ if (e.shiftKey && (active === first || active === root)) {
1528
+ last.focus();
1529
+ e.preventDefault();
1530
+ } else if (!e.shiftKey && active === last) {
1531
+ first.focus();
1532
+ e.preventDefault();
1533
+ }
1534
+ return;
1535
+ }
1536
+ switch (e.key) {
1537
+ case "ArrowRight":
1538
+ case "ArrowLeft":
1539
+ case "ArrowUp":
1540
+ case "ArrowDown": {
1541
+ const flip = isRtl() && (e.key === "ArrowRight" || e.key === "ArrowLeft");
1542
+ const k = flip ? e.key === "ArrowRight" ? "ArrowLeft" : "ArrowRight" : e.key;
1543
+ arrowNav[k]?.();
1544
+ break;
1545
+ }
1546
+ case "Escape":
1547
+ if (!e.defaultPrevented) viewer.close();
1548
+ break;
1549
+ case "+":
1550
+ case "=":
1551
+ zoomAroundCenter(1.25);
1552
+ break;
1553
+ case "-":
1554
+ zoomAroundCenter(1 / 1.25);
1555
+ break;
1556
+ case "0":
1557
+ viewer.resetZoom();
1558
+ break;
1559
+ case "t":
1560
+ case "T":
1561
+ setRailCollapsed(!railCollapsed, true);
1562
+ break;
1563
+ case "Home":
1564
+ if (linear) {
1565
+ ensureFlat();
1566
+ const t = flatSeq[0];
1567
+ if (t) viewer.goTo(t.g, t.i);
1568
+ } else viewer.goTo(0);
1569
+ break;
1570
+ case "End":
1571
+ if (linear) {
1572
+ ensureFlat();
1573
+ const t = flatSeq[flatSeq.length - 1];
1574
+ if (t) viewer.goTo(t.g, t.i);
1575
+ } else viewer.goTo(viewer.getCollection().groups.length - 1);
1576
+ break;
1577
+ default:
1578
+ if (e.ctrlKey || e.metaKey || e.altKey) return;
1579
+ if (!linear && /^[1-9]$/.test(e.key)) {
1580
+ viewer.goTo(viewer.getState().cursor.g, Number(e.key) - 1);
1581
+ } else if (pluginKeymap[e.key]) {
1582
+ pluginKeymap[e.key]?.(e);
1583
+ }
1584
+ return;
1585
+ }
1586
+ e.preventDefault();
1587
+ };
1588
+ root.addEventListener("keydown", onKey);
1589
+ const isOurFullscreen = () => {
1590
+ const fe = document.fullscreenElement;
1591
+ return !!fe && (fe === root || root.contains(fe));
1592
+ };
1593
+ const onFsChange = () => {
1594
+ if (isOurFullscreen()) {
1595
+ if (preFsCollapsed === null) preFsCollapsed = railCollapsed;
1596
+ setRailCollapsed(true);
1597
+ } else if (preFsCollapsed !== null) {
1598
+ setRailCollapsed(preFsCollapsed);
1599
+ preFsCollapsed = null;
1600
+ }
1601
+ };
1602
+ document.addEventListener("fullscreenchange", onFsChange);
1603
+ const currentRec = () => {
1604
+ const key = viewer.getCollection().groups[viewer.getState().cursor.g]?.id;
1605
+ return key ? mounted.get(key) : void 0;
1606
+ };
1607
+ const detachGestures = attachGestures({
1608
+ stage,
1609
+ viewer,
1610
+ maxScale: MAX_ZOOM,
1611
+ nav,
1612
+ // route swipes through the linear/oriented nav seam
1613
+ getContent: () => currentRec()?.content ?? null,
1614
+ getGeometry: () => {
1615
+ const r = stage.getBoundingClientRect();
1616
+ return {
1617
+ stageRect: { left: r.left, top: r.top, width: r.width, height: r.height },
1618
+ naturalSize: { w: r.width, h: r.height },
1619
+ mediaBox: mediaBox({ w: r.width, h: r.height })
1620
+ };
1621
+ }
1622
+ });
1623
+ let cancelPhaseWait = null;
1624
+ const awaitPhaseAnimation = (done) => {
1625
+ cancelPhaseWait?.();
1626
+ const style = getComputedStyle(root);
1627
+ if (!style.animationName || style.animationName === "none") return done();
1628
+ const onEnd = (e) => {
1629
+ if (e.target === root) finish();
1630
+ };
1631
+ const stop = () => {
1632
+ root.removeEventListener("animationend", onEnd);
1633
+ clearTimeout(timer);
1634
+ cancelPhaseWait = null;
1635
+ };
1636
+ const finish = () => {
1637
+ stop();
1638
+ done();
1639
+ };
1640
+ root.addEventListener("animationend", onEnd);
1641
+ const timer = setTimeout(finish, animationMs(style) + 50);
1642
+ cancelPhaseWait = stop;
1643
+ };
1644
+ const offLifecycle = internals.setLifecycleDriver({
1645
+ opening: awaitPhaseAnimation,
1646
+ closing: awaitPhaseAnimation
1647
+ });
1648
+ const stageSize = () => {
1649
+ const r = stage.getBoundingClientRect();
1650
+ return { w: r.width, h: r.height };
1651
+ };
1652
+ let frameInset = 0;
1653
+ const readFrameInset = () => {
1654
+ frameInset = frame ? Number.parseFloat(getComputedStyle(root).getPropertyValue("--gallerist-frame-inset")) || 0 : 0;
1655
+ };
1656
+ function paintedBox(size, natural) {
1657
+ const inset = Math.max(0, Math.min(frameInset, size.w / 4, size.h / 4));
1658
+ const inner = { w: size.w - 2 * inset, h: size.h - 2 * inset };
1659
+ if (!(natural && natural.w > 0 && natural.h > 0)) return { x: inset, y: inset, ...inner };
1660
+ const r = containRect(inner, natural);
1661
+ return { x: r.x + inset, y: r.y + inset, w: r.w, h: r.h };
1662
+ }
1663
+ function mediaBox(size) {
1664
+ return paintedBox(size, currentRec()?.media?.naturalSize?.());
1665
+ }
1666
+ function frameSlide(rec) {
1667
+ if (!frame) return;
1668
+ const size = stageSize();
1669
+ if (!(size.w > 0 && size.h > 0)) return;
1670
+ const box = paintedBox(size, rec.media?.naturalSize?.());
1671
+ const s = rec.content.style;
1672
+ s.setProperty("--gallerist-box-x", `${box.x}px`);
1673
+ s.setProperty("--gallerist-box-y", `${box.y}px`);
1674
+ s.setProperty("--gallerist-box-w", `${box.w}px`);
1675
+ s.setProperty("--gallerist-box-h", `${box.h}px`);
1676
+ }
1677
+ const constrainZoom = (z) => {
1678
+ const scaled = { scale: internal.clampScale(z.scale, 1, MAX_ZOOM), x: z.x, y: z.y };
1679
+ const size = stageSize();
1680
+ if (!(size.w > 0 && size.h > 0)) return scaled;
1681
+ return internal.clampPanToBox(scaled, size, mediaBox(size));
1682
+ };
1683
+ const offZoomBounds = internals.setZoomBounds({
1684
+ center: () => {
1685
+ const size = stageSize();
1686
+ return { x: size.w / 2, y: size.h / 2 };
1687
+ },
1688
+ constrain: constrainZoom
1689
+ });
1690
+ const reclampZoom = () => {
1691
+ const z = viewer.getState().zoom;
1692
+ const clamped = constrainZoom(z);
1693
+ if (clamped.scale !== z.scale || clamped.x !== z.x || clamped.y !== z.y) {
1694
+ internals.setZoom(clamped);
1695
+ } else {
1696
+ renderWindow();
1697
+ updateNavigator();
1698
+ }
1699
+ };
1700
+ let resizeFrame = 0;
1701
+ const onStageResize = () => {
1702
+ if (resizeFrame) return;
1703
+ resizeFrame = requestAnimationFrame(() => {
1704
+ resizeFrame = 0;
1705
+ if (!attached) return;
1706
+ readFrameInset();
1707
+ for (const rec of mounted.values()) frameSlide(rec);
1708
+ const cur = currentRec();
1709
+ if (cur) cur.overlayItem = void 0;
1710
+ reclampZoom();
1711
+ });
1712
+ };
1713
+ const resizeObserver = typeof ResizeObserver !== "undefined" ? new ResizeObserver(onStageResize) : null;
1714
+ resizeObserver?.observe(stage);
1715
+ let insetFrame = 0;
1716
+ const updateControlsInset = () => {
1717
+ insetFrame = 0;
1718
+ if (!controlsBox) return;
1719
+ if (root.getAttribute("data-gallerist-preset") !== "immersive" || !attached) {
1720
+ controlsBox.style.removeProperty("--gallerist-controls-inset");
1721
+ return;
1722
+ }
1723
+ const m = main.getBoundingClientRect();
1724
+ let top = 0;
1725
+ let right = 0;
1726
+ let bottom = 0;
1727
+ let left = 0;
1728
+ for (const band of [
1729
+ bar,
1730
+ caption,
1731
+ regionHosts.footer,
1732
+ regionHosts.thumbnails,
1733
+ regionHosts.aside
1734
+ ]) {
1735
+ if (band.hidden) continue;
1736
+ const r = band.getBoundingClientRect();
1737
+ if (r.width < 1 || r.height < 1) continue;
1738
+ if (r.width >= m.width * 0.5) {
1739
+ if (r.top - m.top < m.bottom - r.bottom) top = Math.max(top, r.bottom - m.top);
1740
+ else bottom = Math.max(bottom, m.bottom - r.top);
1741
+ } else if (r.left - m.left < m.right - r.right) {
1742
+ left = Math.max(left, r.right - m.left);
1743
+ } else {
1744
+ right = Math.max(right, m.right - r.left);
1745
+ }
1746
+ }
1747
+ controlsBox.style.setProperty(
1748
+ "--gallerist-controls-inset",
1749
+ `${top}px ${right}px ${bottom}px ${left}px`
1750
+ );
1751
+ };
1752
+ const scheduleInset = () => {
1753
+ if (!insetFrame) insetFrame = requestAnimationFrame(updateControlsInset);
1754
+ };
1755
+ const bandObserver = controlsBox && typeof ResizeObserver !== "undefined" ? new ResizeObserver(scheduleInset) : null;
1756
+ for (const band of [
1757
+ main,
1758
+ bar,
1759
+ caption,
1760
+ regionHosts.footer,
1761
+ regionHosts.thumbnails,
1762
+ regionHosts.aside
1763
+ ]) {
1764
+ bandObserver?.observe(band);
1765
+ }
1766
+ const offRefresh = internals.onRefresh(onStageResize);
1767
+ const mountPluginRegions = createRegionMounter(regionHosts, { viewer, preset });
1768
+ const mountRegions = () => {
1769
+ pluginKeymap = aggregateKeymap(internals.getPlugins());
1770
+ mountPluginRegions(internals.getPlugins());
1771
+ const on = opts.linear ?? !!regionHosts.thumbnails.querySelector("[data-gallerist-linear]");
1772
+ root.toggleAttribute("data-gallerist-arrow-labels", !!arrowLabel && !on);
1773
+ if (on !== linear) {
1774
+ linear = on;
1775
+ if (on) root.setAttribute("data-gallerist-linear", "true");
1776
+ else root.removeAttribute("data-gallerist-linear");
1777
+ chromeCol = null;
1778
+ }
1779
+ const thumbs = regionHosts.thumbnails;
1780
+ const hasRail = [...thumbs.childNodes].some((n) => n !== railHandle);
1781
+ if (!hasRail) railHandle.remove();
1782
+ else if (thumbs.firstChild !== railHandle) thumbs.prepend(railHandle);
1783
+ if (!controls) bar.hidden = regionHosts.toolbar.childElementCount === 0;
1784
+ syncCaption();
1785
+ const cur = currentRec();
1786
+ if (cur) cur.overlayItem = void 0;
1787
+ if (attached) {
1788
+ renderWindow();
1789
+ updateChrome();
1790
+ }
1791
+ };
1792
+ mountRegions();
1793
+ const offPlugins = internals.onPluginsChanged(mountRegions);
1794
+ const unsubscribe = viewer.subscribe(sync);
1795
+ sync();
1796
+ return {
1797
+ root,
1798
+ destroy() {
1799
+ unsubscribe();
1800
+ offPlugins();
1801
+ offZoomBounds();
1802
+ offRefresh();
1803
+ cancelPhaseWait?.();
1804
+ offLifecycle();
1805
+ detachGestures();
1806
+ resizeObserver?.disconnect();
1807
+ bandObserver?.disconnect();
1808
+ capObserver?.disconnect();
1809
+ if (insetFrame) cancelAnimationFrame(insetFrame);
1810
+ if (resizeFrame) cancelAnimationFrame(resizeFrame);
1811
+ idle?.destroy();
1812
+ document.removeEventListener("fullscreenchange", onFsChange);
1813
+ root.removeEventListener("keydown", onKey);
1814
+ for (const rec of mounted.values()) teardownSlide(rec);
1815
+ mounted.clear();
1816
+ warmRun++;
1817
+ for (const [id, img] of warm) unwarm(id, img);
1818
+ detach();
1819
+ if (!overlay) root.remove();
1820
+ }
1821
+ };
1822
+ }
1823
+
1824
+ // src/bind.ts
1825
+ var attr = (el2, name) => el2.getAttribute(`data-gallerist-${name}`) ?? void 0;
1826
+ var dimension = (el2, name) => Number(attr(el2, name));
1827
+ function itemFromLink(el2, id) {
1828
+ const source = attr(el2, "src") ?? el2.getAttribute("href");
1829
+ if (!source) return null;
1830
+ const img = el2.querySelector("img");
1831
+ const item = {
1832
+ id: attr(el2, "id") ?? id,
1833
+ type: attr(el2, "type") ?? "image",
1834
+ source
1835
+ };
1836
+ const thumb = attr(el2, "thumb") ?? img?.getAttribute("src");
1837
+ if (thumb) item.thumb = thumb;
1838
+ const alt = attr(el2, "alt") ?? img?.getAttribute("alt");
1839
+ if (alt) item.alt = alt;
1840
+ const caption = attr(el2, "caption");
1841
+ if (caption) item.caption = caption;
1842
+ const w = dimension(el2, "width");
1843
+ const h = dimension(el2, "height");
1844
+ if (w > 0 && h > 0) item.naturalSize = { w, h };
1845
+ return item;
1846
+ }
1847
+ function collectionFromLinks(links, idPrefix = "link") {
1848
+ const groups = [];
1849
+ const byId = /* @__PURE__ */ new Map();
1850
+ const positions = /* @__PURE__ */ new Map();
1851
+ const usedIds = /* @__PURE__ */ new Set();
1852
+ let n = 0;
1853
+ for (const el2 of links) {
1854
+ const generated = `${idPrefix}-${n}`;
1855
+ const item = itemFromLink(el2, generated);
1856
+ if (!item) continue;
1857
+ if (usedIds.has(item.id)) item.id = usedIds.has(generated) ? `${generated}-dup` : generated;
1858
+ usedIds.add(item.id);
1859
+ const groupId = attr(el2, "group");
1860
+ let g = groupId ? byId.get(groupId) : void 0;
1861
+ if (g === void 0) {
1862
+ const group = { id: groupId ?? `${idPrefix}-g${n}`, items: [] };
1863
+ const caption = attr(el2, "group-caption");
1864
+ if (caption) group.caption = caption;
1865
+ const subtitle = attr(el2, "group-subtitle");
1866
+ if (subtitle) group.meta = { subtitle };
1867
+ g = groups.push(group) - 1;
1868
+ if (groupId) byId.set(groupId, g);
1869
+ }
1870
+ const items = groups[g].items;
1871
+ positions.set(el2, { g, i: items.push(item) - 1 });
1872
+ n++;
1873
+ }
1874
+ return { collection: { groups }, positions };
1875
+ }
1876
+ function bind(opts = {}) {
1877
+ const selector = opts.selector ?? "[data-gallerist]";
1878
+ const root = opts.root ?? document;
1879
+ const live = /* @__PURE__ */ new Map();
1880
+ let destroyed = false;
1881
+ const linksOf = (gallery) => [...root.querySelectorAll(selector)].filter(
1882
+ (el2) => (el2.getAttribute("data-gallerist") ?? "") === gallery && !el2.closest(".gallerist")
1883
+ );
1884
+ const pick = (value, gallery) => typeof value === "function" ? value(gallery) : value;
1885
+ const ensure = (gallery) => {
1886
+ const { collection, positions } = collectionFromLinks(linksOf(gallery), gallery || "link");
1887
+ if (collection.groups.length === 0) return null;
1888
+ const signature = JSON.stringify(collection);
1889
+ const existing = live.get(gallery);
1890
+ if (existing) {
1891
+ if (existing.signature !== signature) {
1892
+ existing.viewer.setCollection(collection);
1893
+ existing.signature = signature;
1894
+ }
1895
+ return { entry: existing, positions };
1896
+ }
1897
+ const viewer = core.createViewer({ ...pick(opts.viewer, gallery), collection });
1898
+ const handle = mount(viewer, {
1899
+ // a link removed meanwhile falls back to what had focus on opening
1900
+ returnFocus: () => {
1901
+ const link = live.get(gallery)?.link;
1902
+ return link?.isConnected ? link : null;
1903
+ },
1904
+ ...pick(opts.mount, gallery)
1905
+ });
1906
+ const entry = { viewer, handle, signature, link: null };
1907
+ live.set(gallery, entry);
1908
+ return { entry, positions };
1909
+ };
1910
+ const onClick = (event) => {
1911
+ const e = event;
1912
+ if (e.defaultPrevented || e.button !== 0) return;
1913
+ if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return;
1914
+ const link = e.target?.closest?.(selector);
1915
+ if (!link || link.closest(".gallerist")) return;
1916
+ const ready = ensure(link.getAttribute("data-gallerist") ?? "");
1917
+ const at = ready?.positions.get(link);
1918
+ if (!ready || !at) return;
1919
+ e.preventDefault();
1920
+ ready.entry.link = link;
1921
+ ready.entry.viewer.open(at);
1922
+ };
1923
+ root.addEventListener("click", onClick);
1924
+ return {
1925
+ open(gallery, at) {
1926
+ if (destroyed) return;
1927
+ const ready = ensure(gallery ?? "");
1928
+ if (!ready) return;
1929
+ ready.entry.link = null;
1930
+ ready.entry.viewer.open(at ?? { g: 0, i: 0 });
1931
+ },
1932
+ viewer: (gallery = "") => live.get(gallery)?.viewer,
1933
+ destroy() {
1934
+ destroyed = true;
1935
+ root.removeEventListener("click", onClick);
1936
+ for (const { handle, viewer } of live.values()) {
1937
+ handle.destroy();
1938
+ viewer.destroy();
1939
+ }
1940
+ live.clear();
1941
+ }
1942
+ };
1943
+ }
1944
+
1945
+ // src/index.ts
1946
+ var VERSION = "0.3.1" ;
1947
+
1948
+ Object.defineProperty(exports, "reconcileKeys", {
1949
+ enumerable: true,
1950
+ get: function () { return internal.reconcileKeys; }
1951
+ });
1952
+ Object.defineProperty(exports, "windowIndices", {
1953
+ enumerable: true,
1954
+ get: function () { return internal.windowIndices; }
1955
+ });
1956
+ exports.DEFAULT_STRINGS = DEFAULT_STRINGS;
1957
+ exports.VERSION = VERSION;
1958
+ exports.applyAppearance = applyAppearance;
1959
+ exports.bind = bind;
1960
+ exports.collectionFromLinks = collectionFromLinks;
1961
+ exports.imageAdapter = imageAdapter;
1962
+ exports.mount = mount;
1963
+ exports.resolvePreset = resolvePreset;
1964
+ //# sourceMappingURL=index.cjs.map
1965
+ //# sourceMappingURL=index.cjs.map