@michaelyagi/shoji 0.1.0-alpha.37 → 0.1.0-alpha.38

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/esm/index.js CHANGED
@@ -6,7 +6,7 @@ import { Layout } from "./plugins/layout/index.js";
6
6
  import { RotateFlip } from "./plugins/rotateFlip/index.js";
7
7
  import { Video } from "./plugins/video/index.js";
8
8
  import { Zoom } from "./plugins/zoom/index.js";
9
- const version = "0.1.0-alpha.37";
9
+ const version = "0.1.0-alpha.38";
10
10
  const Shoji = Object.assign(Gallery, {
11
11
  Autoplay,
12
12
  Layout,
@@ -6,12 +6,14 @@ export interface AutoplayOptions {
6
6
  showProgress?: boolean;
7
7
  /** Starts the slideshow automatically as soon as the gallery opens — every `open()`, not just the first — instead of waiting for the toolbar button/`Space`. Default `false`. */
8
8
  autoStart?: boolean;
9
- /** Pauses the slideshow while the viewer is zoomed in on the active slide (Zoom plugin) — stays paused until Play is pressed again, even once un-zoomed back to neutral (no auto-resume). A no-op if Zoom isn't loaded. Default `false` — opt-in, same reasoning as `pauseOnRotateFlip` below. */
9
+ /** Pauses the slideshow while the viewer is zoomed in on the active slide (Zoom plugin) — stays paused until Play is pressed again, even once un-zoomed back to neutral (no auto-resume). A no-op if Zoom isn't loaded. Default `true` — a viewer actively examining a zoomed-in detail is a clear signal the slideshow shouldn't advance out from under them; set `false` to let it keep running regardless. */
10
10
  pauseOnZoom?: boolean;
11
- /** Pauses the slideshow on any RotateFlip interaction — including one that lands back on the original orientation, since the click itself is still an active interruption, not just its end state. Stays paused until Play is pressed again. A no-op if RotateFlip isn't loaded. Default `false` — opt-in, requested directly: a host embedding a gallery shouldn't have its slideshow's pause behavior silently change just because a viewer happens to touch a zoom/rotate control, unless it asked for that. */
11
+ /** Pauses the slideshow on any RotateFlip interaction — including one that lands back on the original orientation, since the click itself is still an active interruption, not just its end state. Stays paused until Play is pressed again. A no-op if RotateFlip isn't loaded. Default `true`, same reasoning as `pauseOnZoom` above — a viewer reaching for a rotate/flip control is actively engaging with the current slide, not passively watching a slideshow; set `false` to let it keep running regardless. */
12
12
  pauseOnRotateFlip?: boolean;
13
- /** Pauses the slideshow when the viewer expands a truncated caption to read the rest (core's own caption modal, DESIGN.md §2.3a) — stays paused until Play is pressed again, same as `pauseOnZoom`/`pauseOnRotateFlip`. No re-check on Play needed the way those two need one: the modal traps both pointer and keyboard input while open (core blocks every key, not just Escape), so Play is physically unreachable until the modal is already closed. Default `false` — same opt-in reasoning as the other two. */
13
+ /** Pauses the slideshow when the viewer expands a truncated caption to read the rest (core's own caption modal, DESIGN.md §2.3a) — stays paused until Play is pressed again, same as `pauseOnZoom`/`pauseOnRotateFlip`. No re-check on Play needed the way those two need one: the modal traps both pointer and keyboard input while open (core blocks every key, not just Escape), so Play is physically unreachable until the modal is already closed. Default `true`, same reasoning as the other two — expanding a caption to read the rest is a request for time, not an idle moment to advance past; set `false` to let it keep running regardless. */
14
14
  pauseOnCaptionExpand?: boolean;
15
+ /** Pauses the slideshow the moment the viewer navigates manually — arrow keys/buttons, a completed swipe, a thumbnail click, or any other `goTo()` not caused by Autoplay's own `advance()` — instead of silently re-timing itself on whatever slide they land on. Stays paused until Play is pressed again, same as the other `pauseOn*` options. Default `true`, same reasoning as `pauseOnZoom`/`pauseOnRotateFlip`/`pauseOnCaptionExpand` above — reaching for the navigation controls at all is a clear signal of active engagement, and a slideshow that keeps ticking underneath that reads as broken, not helpful. Set `false` to restore the original "manual nav just re-times the current slide" behavior. */
16
+ pauseOnManualNavigate?: boolean;
15
17
  }
16
18
  /**
17
19
  * DESIGN.md §4-autoplay. Advances on a fixed `interval` (default 5000ms) for
@@ -19,15 +19,23 @@ const PROVIDER_PLAY_RETRY_MS = 400;
19
19
  const MAX_PROVIDER_PLAY_ATTEMPTS = 8;
20
20
  const Autoplay = {
21
21
  name: "autoplay",
22
- defaults: { interval: 5e3, showProgress: true },
22
+ defaults: {
23
+ interval: 5e3,
24
+ showProgress: true,
25
+ pauseOnZoom: true,
26
+ pauseOnRotateFlip: true,
27
+ pauseOnCaptionExpand: true,
28
+ pauseOnManualNavigate: true
29
+ },
23
30
  init(ctx) {
24
31
  const { gallery } = ctx;
25
32
  const interval = Number(ctx.options.interval ?? 5e3);
26
33
  const showProgress = ctx.options.showProgress !== false;
27
34
  const autoStart = ctx.options.autoStart === true;
28
- const pauseOnZoom = ctx.options.pauseOnZoom === true;
29
- const pauseOnRotateFlip = ctx.options.pauseOnRotateFlip === true;
30
- const pauseOnCaptionExpand = ctx.options.pauseOnCaptionExpand === true;
35
+ const pauseOnZoom = ctx.options.pauseOnZoom !== false;
36
+ const pauseOnRotateFlip = ctx.options.pauseOnRotateFlip !== false;
37
+ const pauseOnCaptionExpand = ctx.options.pauseOnCaptionExpand !== false;
38
+ const pauseOnManualNavigate = ctx.options.pauseOnManualNavigate !== false;
31
39
  const locale = ctx.options.locale;
32
40
  const playLabel = (locale == null ? void 0 : locale.play) ?? "Play slideshow";
33
41
  const pauseLabel = (locale == null ? void 0 : locale.pause) ?? "Pause slideshow";
@@ -38,6 +46,7 @@ const Autoplay = {
38
46
  let wasPlayingBeforeDrag = false;
39
47
  let zoomedIn = false;
40
48
  let rotatedOrFlipped = false;
49
+ let isAdvancing = false;
41
50
  const outer = ctx.ui.outer();
42
51
  function onVideoError(event) {
43
52
  if (!playing) return;
@@ -163,7 +172,9 @@ const Autoplay = {
163
172
  }
164
173
  function advance() {
165
174
  const before = gallery.currentIndex;
175
+ isAdvancing = true;
166
176
  gallery.next();
177
+ isAdvancing = false;
167
178
  if (gallery.currentIndex === before) stop();
168
179
  }
169
180
  function start() {
@@ -223,7 +234,9 @@ const Autoplay = {
223
234
  zoomedIn = false;
224
235
  rotatedOrFlipped = false;
225
236
  updateToggleAvailability();
226
- if (playing) enterSlide();
237
+ if (!playing) return;
238
+ if (pauseOnManualNavigate && !isAdvancing) stop();
239
+ else enterSlide();
227
240
  });
228
241
  const offSlideItemLoad = ctx.on("slideItemLoad", ({ index }) => {
229
242
  if (playing && awaitingProviderVideo && index === gallery.currentIndex) enterSlide();
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sources":["../../../../src/plugins/autoplay/icons.ts","../../../../src/plugins/autoplay/index.ts"],"sourcesContent":["/** DESIGN.md §9 — inline SVG, stroke/fill = currentColor, matches src/core/icons.ts's convention. */\n\nexport const PLAY_ICON =\n '<svg viewBox=\"0 0 24 24\" width=\"24\" height=\"24\" fill=\"currentColor\"><path d=\"M8 5v14l11-7z\"/></svg>';\n\nexport const PAUSE_ICON =\n '<svg viewBox=\"0 0 24 24\" width=\"24\" height=\"24\" fill=\"currentColor\"><path d=\"M7 5h4v14H7zM13 5h4v14h-4z\"/></svg>';\n","import type { PluginContext, ShojiPlugin } from '../../core/plugin';\nimport { PAUSE_ICON, PLAY_ICON } from './icons';\nimport './autoplay.css';\n\n/**\n * A native `<video>` or a `.shoji-slide-provider-video` container a video\n * provider plugin (§4-video) has augmented with the same shape — Autoplay\n * never needs to know which. `play()`'s return type is technically a lie\n * for the provider case (that `.play` is a synchronous fire-and-forget, no\n * `HTMLVideoElement.play()`-style promise) — harmless, `enterSlide()`'s own\n * `typeof playResult.catch === 'function'` guard already treats a\n * non-promise `play()` result as \"nothing to await,\" same as `undefined`.\n */\ntype PlayableMedia = Pick<\n HTMLVideoElement,\n 'play' | 'pause' | 'paused' | 'ended' | 'muted' | 'addEventListener' | 'removeEventListener'\n>;\n\n/** `.shoji-slide-provider-video` only counts if it's actually been wired up as playable (§4-video's `wirePlayableContract`) — a provider still mid-async-setup, or one that never opted into Autoplay sync at all, isn't. */\nfunction findPlayable(media: HTMLElement | null): PlayableMedia | null {\n const video = media?.querySelector('video');\n if (video) return video;\n const provider = media?.querySelector<HTMLElement & Partial<PlayableMedia>>(\n '.shoji-slide-provider-video',\n );\n if (provider && typeof provider.play === 'function') return provider as PlayableMedia;\n return null;\n}\n\n/** A provider container is attached to the DOM immediately (§4-video), well before its async setup (loading an SDK, constructing a player) finishes wiring `.play` — this is what tells `enterSlide()` \"there's a video here, it's just not playable yet\" apart from \"no video at all,\" so it doesn't mistreat a still-loading video as an ordinary timed slide. */\nfunction isPendingProviderVideo(media: HTMLElement | null): boolean {\n const provider = media?.querySelector<HTMLElement & Partial<PlayableMedia>>(\n '.shoji-slide-provider-video',\n );\n return !!provider && typeof provider.play !== 'function';\n}\n\n// A provider embed's postMessage bridge (DESIGN.md §4.3) can need more real\n// time after \"ready\" before it reliably processes its first command — a\n// play() issued too soon can silently no-op with nothing to catch. Retries\n// a few times with a short delay instead of a single best-effort attempt.\nconst PROVIDER_PLAY_RETRY_MS = 400;\nconst MAX_PROVIDER_PLAY_ATTEMPTS = 8; // + the initial attempt = 9 total, ~3.6s before giving up\n\nexport interface AutoplayOptions {\n /** Milliseconds between advances for timed (photo) slides. Default `5000`. */\n interval?: number;\n /** Shows the thin progress bar (`--shoji-progress`) tracking time-to-next-advance along the dialog's bottom edge, for timed slides only — never shown during video slides, whose own runtime drives advancement instead. Default `true`. Purely presentational: turning it off doesn't change any timing, only whether it's drawn. */\n showProgress?: boolean;\n /** Starts the slideshow automatically as soon as the gallery opens — every `open()`, not just the first — instead of waiting for the toolbar button/`Space`. Default `false`. */\n autoStart?: boolean;\n /** Pauses the slideshow while the viewer is zoomed in on the active slide (Zoom plugin) — stays paused until Play is pressed again, even once un-zoomed back to neutral (no auto-resume). A no-op if Zoom isn't loaded. Default `false` — opt-in, same reasoning as `pauseOnRotateFlip` below. */\n pauseOnZoom?: boolean;\n /** Pauses the slideshow on any RotateFlip interaction — including one that lands back on the original orientation, since the click itself is still an active interruption, not just its end state. Stays paused until Play is pressed again. A no-op if RotateFlip isn't loaded. Default `false` — opt-in, requested directly: a host embedding a gallery shouldn't have its slideshow's pause behavior silently change just because a viewer happens to touch a zoom/rotate control, unless it asked for that. */\n pauseOnRotateFlip?: boolean;\n /** Pauses the slideshow when the viewer expands a truncated caption to read the rest (core's own caption modal, DESIGN.md §2.3a) — stays paused until Play is pressed again, same as `pauseOnZoom`/`pauseOnRotateFlip`. No re-check on Play needed the way those two need one: the modal traps both pointer and keyboard input while open (core blocks every key, not just Escape), so Play is physically unreachable until the modal is already closed. Default `false` — same opt-in reasoning as the other two. */\n pauseOnCaptionExpand?: boolean;\n}\n\n/**\n * DESIGN.md §4-autoplay. Advances on a fixed `interval` (default 5000ms) for\n * ordinary slides; for a video slide, plays it and waits for `ended` instead\n * — the interval never applies to video. A manual pause on that video pauses\n * the *slideshow* too (not just the video); manually resuming the video does\n * NOT resume the slideshow — that requires pressing the slideshow's own\n * play control again. See `enterSlide()`/`onVideoPause()` below for exactly\n * where each half of that rule lives.\n */\nexport const Autoplay: ShojiPlugin = {\n name: 'autoplay',\n defaults: { interval: 5000, showProgress: true } satisfies AutoplayOptions,\n\n init(ctx: PluginContext): () => void {\n const { gallery } = ctx;\n const interval = Number(ctx.options.interval ?? 5000);\n const showProgress = ctx.options.showProgress !== false;\n const autoStart = ctx.options.autoStart === true;\n const pauseOnZoom = ctx.options.pauseOnZoom === true;\n const pauseOnRotateFlip = ctx.options.pauseOnRotateFlip === true;\n const pauseOnCaptionExpand = ctx.options.pauseOnCaptionExpand === true;\n const locale = ctx.options.locale as Partial<Record<'play' | 'pause', string>> | undefined;\n const playLabel = locale?.play ?? 'Play slideshow';\n const pauseLabel = locale?.pause ?? 'Pause slideshow';\n\n let playing = false;\n let timer: ReturnType<typeof setTimeout> | null = null;\n let currentVideo: PlayableMedia | null = null;\n let awaitingProviderVideo = false;\n let wasPlayingBeforeDrag = false;\n // Tracks only the *current* zoom/rotate-flip state, purely for\n // toggle()'s \"was it already engaged the moment Play was pressed\"\n // check below — not reused for the pause-on-event logic itself, which\n // reacts to each zoomChange/rotateFlipChange event directly instead of\n // consulting these.\n let zoomedIn = false;\n let rotatedOrFlipped = false;\n\n // A real bug, regression: this used to capture gallery.getActiveMedia()\n // once here and listen on that node directly, back when a pool slot's\n // own offset never changed after construction (only its *content*\n // moved between slots) — so \"whichever slot has offset 0\" was always\n // the same physical node, safe to capture once. SlideManager's pool now\n // relabels a slot's offset in place instead (§2.3) — the node that\n // happened to be offset 0 when this plugin initialized can become a\n // neighbor after even one navigation, while a *different* node becomes\n // the active one, silently going unheard by this listener. Listening on\n // `ctx.ui.outer()` instead (the whole lightbox, never relabeled) still\n // catches a provider's own error event (§4-video's `onError`, dispatched\n // with `bubbles: true`) regardless of which slide it came from — the\n // `contains()` check below is what actually scopes it to the currently\n // active slide, re-resolved fresh on every error rather than trusting a\n // stale reference.\n const outer = ctx.ui.outer();\n function onVideoError(event: Event): void {\n if (!playing) return;\n const active = gallery.getActiveMedia();\n if (active && event.target instanceof Node && active.contains(event.target)) advance();\n }\n outer.addEventListener('error', onVideoError);\n\n const button = document.createElement('button');\n button.type = 'button';\n // shoji-autoplay-toggle: a stable hook for host code that needs to find\n // this button from outside (e.g. to pause the slideshow before opening\n // its own UI on top) — `title`/`aria-label` swap with `locale`, so\n // matching on those breaks silently the moment a host customizes it.\n button.className = 'shoji-toolbar-button shoji-autoplay-toggle';\n button.setAttribute('aria-label', playLabel);\n button.title = playLabel;\n button.innerHTML = PLAY_ICON;\n\n const progress = document.createElement('div');\n progress.className = 'shoji-autoplay-progress';\n progress.hidden = true;\n const progressBar = document.createElement('div');\n progressBar.className = 'shoji-autoplay-progress-bar';\n progress.appendChild(progressBar);\n\n function clearTimer(): void {\n if (timer !== null) {\n clearTimeout(timer);\n timer = null;\n }\n }\n\n function resetProgressBar(): void {\n if (!showProgress) return;\n progress.hidden = true;\n progressBar.style.transition = 'none';\n progressBar.style.width = '0%';\n }\n\n function runProgressBar(ms: number): void {\n if (!showProgress) return;\n progress.hidden = false;\n progressBar.style.transition = 'none';\n progressBar.style.width = '0%';\n void progressBar.offsetWidth; // commit the reset before transitioning\n progressBar.style.transition = `width ${ms}ms linear`;\n progressBar.style.width = '100%';\n }\n\n /**\n * DESIGN.md §4.1 — while `pauseOnZoom`/`pauseOnRotateFlip` is holding\n * playback paused, pressing Play would otherwise just silently\n * re-pause it in the same synchronous tick (`toggle()`'s own check,\n * below) — no `transitionend`, no paint in between the two state\n * flips, so the button never visibly shows \"Pause\" at all before\n * reverting. Reported as looking broken, not just quiet. Disabling\n * the button while blocked (same `aria-disabled`/`tabIndex`/CSS\n * pattern as core's own slide-loading disable, Gallery.ts's\n * `setSlideLoading()`) makes \"you can't resume yet\" an honest, visible\n * state instead of a click that does nothing. Only while *not*\n * playing — while playing, the button always shows/means Pause, and\n * must stay clickable to stop regardless of zoom/rotate state.\n */\n function updateToggleAvailability(): void {\n const blocked =\n !playing && ((pauseOnZoom && zoomedIn) || (pauseOnRotateFlip && rotatedOrFlipped));\n button.ariaDisabled = blocked ? 'true' : null;\n if (blocked) button.tabIndex = -1;\n else button.removeAttribute('tabindex');\n }\n\n function setButtonState(isPlaying: boolean): void {\n playing = isPlaying;\n button.innerHTML = isPlaying ? PAUSE_ICON : PLAY_ICON;\n button.setAttribute('aria-label', isPlaying ? pauseLabel : playLabel);\n button.title = isPlaying ? pauseLabel : playLabel;\n updateToggleAvailability();\n }\n\n function onVideoEnded(): void {\n if (playing) advance();\n }\n\n // A real user pause, as opposed to the pause some browsers fire\n // immediately alongside 'ended' — `.ended` disambiguates the two.\n // Provider videos (§4.3's renderers) are responsible for holding their\n // own 'pause' dispatch back if it might just be a natural end arriving\n // slightly early — vimeo.ts's `PAUSE_DISPATCH_DELAY_MS` is why.\n function onVideoPause(): void {\n if (currentVideo?.ended) return;\n stop();\n }\n\n function detachVideo(): void {\n if (!currentVideo) return;\n currentVideo.removeEventListener('ended', onVideoEnded);\n currentVideo.removeEventListener('pause', onVideoPause);\n currentVideo = null;\n }\n\n // A provider embed (e.g. YouTube) is cross-origin — unlike native\n // <video>, its own autoplay policy requires a *direct* user gesture on\n // the embed itself, which an automatic play() arriving via this\n // timer/'ended'/slideItemLoad chain never has; it silently no-ops\n // rather than rejecting, so there's nothing to catch. Muting first is\n // what actually gets it to play — the viewer can still unmute via the\n // embed's own controls.\n //\n // A real bug, reported from real usage, found only once a second\n // promise-returning provider (Vimeo) existed to compare against:\n // re-issuing `play()`/`muted = true` on every retry — needed for\n // YouTube, whose fire-and-forget `playVideo()` can silently drop a\n // command issued before its postMessage bridge is fully settled — does\n // the opposite for a provider whose `play()` genuinely returns a\n // promise. Confirmed directly, isolated from Shoji entirely: a single,\n // uninterrupted `play()` call reliably started Vimeo playback in\n // ~1.6s, while calling it again every 400ms (this loop's original,\n // unconditional behavior) kept it stuck indefinitely — each new call\n // resets the progress the previous one had already made, so it never\n // gets an uninterrupted run long enough to actually start. (This is\n // also what point 12 below's retry-exhaustion was actually skipping\n // past: not a broken video, but this loop's own repeated calls\n // preventing it from ever finishing what the first one started.)\n // `video.play()`'s return type only *claims* to be a promise for every\n // provider (see the `PlayableMedia` doc comment above) — branching on\n // whether it genuinely is one is what lets each provider get the\n // retry behavior it actually needs from this one shared function.\n function ensureProviderPlaying(video: PlayableMedia, attemptsLeft: number): void {\n video.muted = true;\n const playResult = video.play();\n if (playResult && typeof playResult.then === 'function') {\n pollWithoutReissuing(video, attemptsLeft, playResult);\n } else {\n reissueOnEachRetry(video, attemptsLeft);\n }\n }\n\n /** Fire-and-forget `play()` (YouTube) — re-issues the command itself on every retry, per this function's own doc comment above. */\n function reissueOnEachRetry(video: PlayableMedia, attemptsLeft: number): void {\n setTimeout(() => {\n if (currentVideo !== video || !playing) return; // stale — slide changed, or already stopped\n if (!video.paused) return; // took effect\n if (attemptsLeft > 0) ensureProviderPlaying(video, attemptsLeft - 1);\n // Exhausted every retry after already muting first — muted\n // autoplay is essentially never blocked by policy, so this means\n // something is genuinely wrong with the embed, not that it's\n // waiting on a gesture. advance() (not stop()) so a slow/late\n // error report (real usage: YouTube's own error can arrive slower\n // than this retry window, e.g. Error 153) can't leave the\n // slideshow stuck if this fires first.\n else advance();\n }, PROVIDER_PLAY_RETRY_MS);\n }\n\n /** A genuine `play()` promise (e.g. Vimeo) — the command itself is only ever issued once (by the caller); this only re-checks `.paused` on the same schedule, over the same total budget, without ever calling `play()` again. */\n function pollWithoutReissuing(\n video: PlayableMedia,\n attemptsLeft: number,\n playResult: Promise<void>,\n ): void {\n // Swallowed deliberately: a rejection here isn't distinguished from\n // \"still pending\" — the .paused poll below is what actually decides\n // whether this took effect, on the same schedule regardless of how\n // the promise itself settles. An unhandled-rejection console warning\n // is the only cost, same tradeoff as the native <video> path below\n // taking a real .catch() instead when it needs to branch on *why*.\n playResult.catch(() => {});\n setTimeout(() => {\n if (currentVideo !== video || !playing) return;\n if (!video.paused) return;\n if (attemptsLeft > 0) pollWithoutReissuing(video, attemptsLeft - 1, playResult);\n else advance();\n }, PROVIDER_PLAY_RETRY_MS);\n }\n\n function enterSlide(): void {\n clearTimer();\n detachVideo();\n resetProgressBar();\n awaitingProviderVideo = false;\n if (!playing) return;\n\n const media = gallery.getActiveMedia();\n const video = findPlayable(media);\n if (video) {\n currentVideo = video;\n video.addEventListener('ended', onVideoEnded);\n video.addEventListener('pause', onVideoPause);\n if (video instanceof HTMLVideoElement) {\n const playResult = video.play();\n // A rejected play() has two different causes that look identical\n // here: the browser blocking an unmuted play() that isn't a direct\n // continuation of a user gesture (NotAllowedError — the video is\n // fine, it just needs the viewer's own click) or the video\n // genuinely being unplayable (NotSupportedError — a broken/missing\n // source, an unsupported format). Only the first pauses the\n // slideshow for the viewer to resolve by hand; anything else means\n // there's nothing to wait for, so it skips ahead instead.\n if (playResult && typeof playResult.catch === 'function') {\n playResult.catch((error: unknown) => {\n if (currentVideo !== video || !playing) return; // stale\n if (error instanceof DOMException && error.name === 'NotAllowedError') stop();\n else advance();\n });\n }\n } else {\n ensureProviderPlaying(video, MAX_PROVIDER_PLAY_ATTEMPTS);\n }\n return;\n }\n\n // A provider video (e.g. YouTube) still mid-setup isn't an ordinary\n // slide either — the slideItemLoad listener below re-enters once it's\n // actually playable. This timer is a fallback in case that never\n // happens (network failure, blocked, ...), so the slideshow can't\n // stall on it forever.\n awaitingProviderVideo = isPendingProviderVideo(media);\n runProgressBar(interval);\n timer = setTimeout(advance, interval);\n }\n\n function advance(): void {\n const before = gallery.currentIndex;\n gallery.next();\n // loop:false and next() was already at the last item — nothing left\n // to advance to; the 'afterSlide' handler below won't fire for a\n // no-op goTo(), so this is the only place that can catch it.\n if (gallery.currentIndex === before) stop();\n }\n\n function start(): void {\n if (playing) return;\n setButtonState(true);\n ctx.emit('autoplayStart', {});\n enterSlide();\n }\n\n function stop(): void {\n if (!playing) return;\n setButtonState(false);\n clearTimer();\n resetProgressBar();\n if (currentVideo && !currentVideo.paused) currentVideo.pause();\n detachVideo();\n ctx.emit('autoplayStop', {});\n }\n\n function toggle(): void {\n if (playing) {\n stop();\n return;\n }\n start();\n // A real bug, reported from real usage: zooming in *first*, then\n // pressing Play, never paused at all — clicking Play doesn't itself\n // fire zoomChange/rotateFlipChange, and a single-step \"toggle back to\n // neutral\" action afterward (Actual size, double-tap-to-reset) only\n // ever emits the *already-neutral* event, never one crossing the\n // engaged threshold — so nothing downstream would ever have caught it\n // either. Re-checked here instead, right after a genuine manual\n // start — the one path a viewer can actually reach a non-neutral view\n // state from *before* pressing Play. `zoomedIn`/`rotatedOrFlipped`\n // (below) track only the *current* state, purely for this check —\n // deliberately not reused for the pause-on-event logic itself, which\n // reacts to each event directly.\n if ((pauseOnZoom && zoomedIn) || (pauseOnRotateFlip && rotatedOrFlipped)) stop();\n }\n\n /**\n * Pauses the slideshow the instant a vertical drag crosses the close\n * threshold, mirroring the controls' own live hide cue\n * (`dragCloseThreshold`) — asked for directly, otherwise the\n * interval/video kept running mid-drag. Resumes only if *this*\n * crossing paused it — a manual pause (toolbar, mid-drag tap) must\n * stay paused even if the drag retreats back under the threshold.\n */\n const offDragThreshold = ctx.on('dragCloseThreshold', ({ hidden }) => {\n if (hidden) {\n wasPlayingBeforeDrag = playing;\n if (playing) stop();\n } else if (wasPlayingBeforeDrag) {\n wasPlayingBeforeDrag = false;\n start();\n }\n });\n\n /**\n * DESIGN.md §4.1 — a real UX gap, not a reported bug: nothing stopped\n * the slideshow from auto-advancing out from under a viewer actively\n * zoomed into a detail on the current slide. Only reacts to\n * `zoomChange`'s event *shape* (`core/types.ts`) — never imports Zoom\n * directly, so this is a no-op with it not loaded, or with\n * `pauseOnZoom: false` (events over inheritance, CLAUDE.md).\n *\n * Deliberately **stays paused** rather than auto-resuming once\n * un-zoomed back to neutral — the first design tried the opposite\n * (edge-tracked \"engaged\" state, auto-resume on disengage, matching\n * `dragCloseThreshold` below) and hit two real problems testing it:\n * (1) a manual restart while still zoomed left the edge-tracker stuck\n * \"already engaged,\" silently skipping the *next* re-pause; (2) once\n * fixed, un-zooming back to exactly scale 1 still auto-resumed even\n * when the viewer's very next action (confirmed directly for\n * RotateFlip's equivalent, below) was to keep interacting with the\n * view controls, not watch the slideshow. `scale > 1` still gates\n * *which* zoomChange events count (an ordinary scale-1 event can also\n * fire from an unrelated slide-change reset, not a real interaction —\n * `zoom/index.ts`'s `reset()` — so it can't unconditionally pause on\n * every event the way rotateFlipChange below safely can).\n */\n const ZOOM_ENGAGED_THRESHOLD = 1.001; // matches Zoom's own ZOOM_EPSILON — \"just barely above 1\" is float residue, not a real zoom\n const offZoomChange = ctx.on('zoomChange', ({ scale }) => {\n zoomedIn = scale > ZOOM_ENGAGED_THRESHOLD;\n if (pauseOnZoom && zoomedIn && playing) stop();\n else updateToggleAvailability(); // stop() above already refreshes this; covers zooming in while already paused, which stop() wouldn't touch\n });\n /**\n * Same UX gap, RotateFlip's own equivalent — off by default (see\n * `pauseOnRotateFlip`'s own doc comment). Pauses on *any*\n * `rotateFlipChange` event unconditionally, including one that lands\n * back on the original orientation — confirmed directly, reported from\n * real usage: rotate four times back to 0deg still reads as an active\n * interruption of the slideshow, not \"nothing happened,\" so it must\n * stay paused too, the same as landing anywhere else. Every\n * `rotateFlipChange` (unlike zoomChange above) only ever fires from a\n * real button/shortcut click — `rotateFlip/index.ts`'s own per-slide\n * `reset()` never emits it — so no extra state check is needed here.\n */\n const offRotateFlipChange = ctx.on('rotateFlipChange', ({ flipH, flipV, rotation }) => {\n rotatedOrFlipped = flipH || flipV || rotation !== 0;\n if (pauseOnRotateFlip && playing) stop();\n else updateToggleAvailability(); // stop() above already refreshes this; covers rotating while already paused, which stop() wouldn't touch\n });\n /**\n * `pauseOnCaptionExpand`'s own doc comment above — unlike the two\n * listeners above, no `updateToggleAvailability()` companion call and\n * no `zoomedIn`/`rotatedOrFlipped`-style tracked flag: the caption\n * modal already makes Play physically unreachable while `open: true`\n * (core's own focus trap + capture-phase keydown blocking, not\n * anything this plugin has to enforce), so there's no \"pressed Play\n * while still blocked\" case to disable the button for or re-check on\n * toggle() the way zoom/rotateFlip both need.\n */\n const offCaptionModalChange = ctx.on('captionModalChange', ({ open }) => {\n if (pauseOnCaptionExpand && open && playing) stop();\n });\n\n button.addEventListener('click', toggle);\n\n // 'right' — clusters immediately before the close button, per DESIGN.md §3.1.\n const removeButton = ctx.ui.toolbar('right', button);\n const removeProgress = showProgress ? ctx.ui.overlay(progress) : null;\n const removeShortcut = ctx.ui.registerShortcut(' ', toggle);\n // Detaches this plugin's own 'pause'/'ended' listeners from the outgoing\n // video before Gallery.navigate() pauses it (see Gallery.ts's comment on\n // this same event) — otherwise that pause is misread as the viewer\n // manually pausing, which stops the slideshow before the new slide's\n // afterSlide handler below ever runs. enterSlide() below also calls\n // detachVideo(), but by then it's too late for *this* transition; that\n // call is what handles the slide *after* this one instead.\n const offBeforeSlide = ctx.on('beforeSlide', () => {\n detachVideo();\n });\n // Any slide change — autoplay's own next(), or the viewer manually\n // navigating mid-slideshow via arrows/buttons/goTo() — re-enters here,\n // tearing down the previous slide's timer/video listeners and setting\n // up fresh ones for whatever is active now. advance() deliberately does\n // NOT call enterSlide() itself: next() already triggers this listener\n // synchronously, so calling it twice would double up the timer/video wiring.\n const offSlide = ctx.on('afterSlide', () => {\n // Zoom/RotateFlip both reset unanimated on navigation (DESIGN.md\n // §2.5) without re-emitting zoomChange/rotateFlipChange — resynced\n // here so a stale \"was engaged\" reading from the *outgoing* slide\n // can't wrongly re-pause toggle()'s next Play press on a slide\n // that's actually neutral now.\n zoomedIn = false;\n rotatedOrFlipped = false;\n updateToggleAvailability();\n if (playing) enterSlide();\n });\n // A provider video (§4-video) that was still mid-setup when enterSlide()\n // last ran — see awaitingProviderVideo there — becomes playable some\n // time after afterSlide already fired and gave up on it for this pass.\n // Re-enter once it's genuinely ready, scoped to the still-active index\n // so a slide the viewer has already moved past doesn't retroactively\n // hijack the timer.\n const offSlideItemLoad = ctx.on('slideItemLoad', ({ index }) => {\n if (playing && awaitingProviderVideo && index === gallery.currentIndex) enterSlide();\n });\n const offClose = ctx.on('close', () => stop());\n const offOpen = ctx.on('afterOpen', () => {\n zoomedIn = false;\n rotatedOrFlipped = false;\n updateToggleAvailability();\n if (autoStart) start();\n });\n\n return () => {\n stop();\n outer.removeEventListener('error', onVideoError);\n removeButton();\n removeProgress?.();\n removeShortcut();\n offBeforeSlide();\n offSlide();\n offSlideItemLoad();\n offOpen();\n offClose();\n offDragThreshold();\n offZoomChange();\n offRotateFlipChange();\n offCaptionModalChange();\n };\n },\n};\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.js","sources":["../../../../src/plugins/autoplay/icons.ts","../../../../src/plugins/autoplay/index.ts"],"sourcesContent":["/** DESIGN.md §9 — inline SVG, stroke/fill = currentColor, matches src/core/icons.ts's convention. */\n\nexport const PLAY_ICON =\n '<svg viewBox=\"0 0 24 24\" width=\"24\" height=\"24\" fill=\"currentColor\"><path d=\"M8 5v14l11-7z\"/></svg>';\n\nexport const PAUSE_ICON =\n '<svg viewBox=\"0 0 24 24\" width=\"24\" height=\"24\" fill=\"currentColor\"><path d=\"M7 5h4v14H7zM13 5h4v14h-4z\"/></svg>';\n","import type { PluginContext, ShojiPlugin } from '../../core/plugin';\nimport { PAUSE_ICON, PLAY_ICON } from './icons';\nimport './autoplay.css';\n\n/**\n * A native `<video>` or a `.shoji-slide-provider-video` container a video\n * provider plugin (§4-video) has augmented with the same shape — Autoplay\n * never needs to know which. `play()`'s return type is technically a lie\n * for the provider case (that `.play` is a synchronous fire-and-forget, no\n * `HTMLVideoElement.play()`-style promise) — harmless, `enterSlide()`'s own\n * `typeof playResult.catch === 'function'` guard already treats a\n * non-promise `play()` result as \"nothing to await,\" same as `undefined`.\n */\ntype PlayableMedia = Pick<\n HTMLVideoElement,\n 'play' | 'pause' | 'paused' | 'ended' | 'muted' | 'addEventListener' | 'removeEventListener'\n>;\n\n/** `.shoji-slide-provider-video` only counts if it's actually been wired up as playable (§4-video's `wirePlayableContract`) — a provider still mid-async-setup, or one that never opted into Autoplay sync at all, isn't. */\nfunction findPlayable(media: HTMLElement | null): PlayableMedia | null {\n const video = media?.querySelector('video');\n if (video) return video;\n const provider = media?.querySelector<HTMLElement & Partial<PlayableMedia>>(\n '.shoji-slide-provider-video',\n );\n if (provider && typeof provider.play === 'function') return provider as PlayableMedia;\n return null;\n}\n\n/** A provider container is attached to the DOM immediately (§4-video), well before its async setup (loading an SDK, constructing a player) finishes wiring `.play` — this is what tells `enterSlide()` \"there's a video here, it's just not playable yet\" apart from \"no video at all,\" so it doesn't mistreat a still-loading video as an ordinary timed slide. */\nfunction isPendingProviderVideo(media: HTMLElement | null): boolean {\n const provider = media?.querySelector<HTMLElement & Partial<PlayableMedia>>(\n '.shoji-slide-provider-video',\n );\n return !!provider && typeof provider.play !== 'function';\n}\n\n// A provider embed's postMessage bridge (DESIGN.md §4.3) can need more real\n// time after \"ready\" before it reliably processes its first command — a\n// play() issued too soon can silently no-op with nothing to catch. Retries\n// a few times with a short delay instead of a single best-effort attempt.\nconst PROVIDER_PLAY_RETRY_MS = 400;\nconst MAX_PROVIDER_PLAY_ATTEMPTS = 8; // + the initial attempt = 9 total, ~3.6s before giving up\n\nexport interface AutoplayOptions {\n /** Milliseconds between advances for timed (photo) slides. Default `5000`. */\n interval?: number;\n /** Shows the thin progress bar (`--shoji-progress`) tracking time-to-next-advance along the dialog's bottom edge, for timed slides only — never shown during video slides, whose own runtime drives advancement instead. Default `true`. Purely presentational: turning it off doesn't change any timing, only whether it's drawn. */\n showProgress?: boolean;\n /** Starts the slideshow automatically as soon as the gallery opens — every `open()`, not just the first — instead of waiting for the toolbar button/`Space`. Default `false`. */\n autoStart?: boolean;\n /** Pauses the slideshow while the viewer is zoomed in on the active slide (Zoom plugin) — stays paused until Play is pressed again, even once un-zoomed back to neutral (no auto-resume). A no-op if Zoom isn't loaded. Default `true` — a viewer actively examining a zoomed-in detail is a clear signal the slideshow shouldn't advance out from under them; set `false` to let it keep running regardless. */\n pauseOnZoom?: boolean;\n /** Pauses the slideshow on any RotateFlip interaction — including one that lands back on the original orientation, since the click itself is still an active interruption, not just its end state. Stays paused until Play is pressed again. A no-op if RotateFlip isn't loaded. Default `true`, same reasoning as `pauseOnZoom` above — a viewer reaching for a rotate/flip control is actively engaging with the current slide, not passively watching a slideshow; set `false` to let it keep running regardless. */\n pauseOnRotateFlip?: boolean;\n /** Pauses the slideshow when the viewer expands a truncated caption to read the rest (core's own caption modal, DESIGN.md §2.3a) — stays paused until Play is pressed again, same as `pauseOnZoom`/`pauseOnRotateFlip`. No re-check on Play needed the way those two need one: the modal traps both pointer and keyboard input while open (core blocks every key, not just Escape), so Play is physically unreachable until the modal is already closed. Default `true`, same reasoning as the other two — expanding a caption to read the rest is a request for time, not an idle moment to advance past; set `false` to let it keep running regardless. */\n pauseOnCaptionExpand?: boolean;\n /** Pauses the slideshow the moment the viewer navigates manually — arrow keys/buttons, a completed swipe, a thumbnail click, or any other `goTo()` not caused by Autoplay's own `advance()` — instead of silently re-timing itself on whatever slide they land on. Stays paused until Play is pressed again, same as the other `pauseOn*` options. Default `true`, same reasoning as `pauseOnZoom`/`pauseOnRotateFlip`/`pauseOnCaptionExpand` above — reaching for the navigation controls at all is a clear signal of active engagement, and a slideshow that keeps ticking underneath that reads as broken, not helpful. Set `false` to restore the original \"manual nav just re-times the current slide\" behavior. */\n pauseOnManualNavigate?: boolean;\n}\n\n/**\n * DESIGN.md §4-autoplay. Advances on a fixed `interval` (default 5000ms) for\n * ordinary slides; for a video slide, plays it and waits for `ended` instead\n * — the interval never applies to video. A manual pause on that video pauses\n * the *slideshow* too (not just the video); manually resuming the video does\n * NOT resume the slideshow — that requires pressing the slideshow's own\n * play control again. See `enterSlide()`/`onVideoPause()` below for exactly\n * where each half of that rule lives.\n */\nexport const Autoplay: ShojiPlugin = {\n name: 'autoplay',\n defaults: {\n interval: 5000,\n showProgress: true,\n pauseOnZoom: true,\n pauseOnRotateFlip: true,\n pauseOnCaptionExpand: true,\n pauseOnManualNavigate: true,\n } satisfies AutoplayOptions,\n\n init(ctx: PluginContext): () => void {\n const { gallery } = ctx;\n const interval = Number(ctx.options.interval ?? 5000);\n const showProgress = ctx.options.showProgress !== false;\n const autoStart = ctx.options.autoStart === true;\n const pauseOnZoom = ctx.options.pauseOnZoom !== false;\n const pauseOnRotateFlip = ctx.options.pauseOnRotateFlip !== false;\n const pauseOnCaptionExpand = ctx.options.pauseOnCaptionExpand !== false;\n const pauseOnManualNavigate = ctx.options.pauseOnManualNavigate !== false;\n const locale = ctx.options.locale as Partial<Record<'play' | 'pause', string>> | undefined;\n const playLabel = locale?.play ?? 'Play slideshow';\n const pauseLabel = locale?.pause ?? 'Pause slideshow';\n\n let playing = false;\n let timer: ReturnType<typeof setTimeout> | null = null;\n let currentVideo: PlayableMedia | null = null;\n let awaitingProviderVideo = false;\n let wasPlayingBeforeDrag = false;\n // Tracks only the *current* zoom/rotate-flip state, purely for\n // toggle()'s \"was it already engaged the moment Play was pressed\"\n // check below — not reused for the pause-on-event logic itself, which\n // reacts to each zoomChange/rotateFlipChange event directly instead of\n // consulting these.\n let zoomedIn = false;\n let rotatedOrFlipped = false;\n // Set for the exact duration of advance()'s own gallery.next() call —\n // the afterSlide handler below fires synchronously inside it, so it can\n // tell \"this slide change is autoplay's own advance()\" apart from any\n // other navigation (arrows/buttons/swipe/goTo()) without needing the\n // event itself to carry a source. See pauseOnManualNavigate above.\n let isAdvancing = false;\n\n // A real bug, regression: this used to capture gallery.getActiveMedia()\n // once here and listen on that node directly, back when a pool slot's\n // own offset never changed after construction (only its *content*\n // moved between slots) — so \"whichever slot has offset 0\" was always\n // the same physical node, safe to capture once. SlideManager's pool now\n // relabels a slot's offset in place instead (§2.3) — the node that\n // happened to be offset 0 when this plugin initialized can become a\n // neighbor after even one navigation, while a *different* node becomes\n // the active one, silently going unheard by this listener. Listening on\n // `ctx.ui.outer()` instead (the whole lightbox, never relabeled) still\n // catches a provider's own error event (§4-video's `onError`, dispatched\n // with `bubbles: true`) regardless of which slide it came from — the\n // `contains()` check below is what actually scopes it to the currently\n // active slide, re-resolved fresh on every error rather than trusting a\n // stale reference.\n const outer = ctx.ui.outer();\n function onVideoError(event: Event): void {\n if (!playing) return;\n const active = gallery.getActiveMedia();\n if (active && event.target instanceof Node && active.contains(event.target)) advance();\n }\n outer.addEventListener('error', onVideoError);\n\n const button = document.createElement('button');\n button.type = 'button';\n // shoji-autoplay-toggle: a stable hook for host code that needs to find\n // this button from outside (e.g. to pause the slideshow before opening\n // its own UI on top) — `title`/`aria-label` swap with `locale`, so\n // matching on those breaks silently the moment a host customizes it.\n button.className = 'shoji-toolbar-button shoji-autoplay-toggle';\n button.setAttribute('aria-label', playLabel);\n button.title = playLabel;\n button.innerHTML = PLAY_ICON;\n\n const progress = document.createElement('div');\n progress.className = 'shoji-autoplay-progress';\n progress.hidden = true;\n const progressBar = document.createElement('div');\n progressBar.className = 'shoji-autoplay-progress-bar';\n progress.appendChild(progressBar);\n\n function clearTimer(): void {\n if (timer !== null) {\n clearTimeout(timer);\n timer = null;\n }\n }\n\n function resetProgressBar(): void {\n if (!showProgress) return;\n progress.hidden = true;\n progressBar.style.transition = 'none';\n progressBar.style.width = '0%';\n }\n\n function runProgressBar(ms: number): void {\n if (!showProgress) return;\n progress.hidden = false;\n progressBar.style.transition = 'none';\n progressBar.style.width = '0%';\n void progressBar.offsetWidth; // commit the reset before transitioning\n progressBar.style.transition = `width ${ms}ms linear`;\n progressBar.style.width = '100%';\n }\n\n /**\n * DESIGN.md §4.1 — while `pauseOnZoom`/`pauseOnRotateFlip` is holding\n * playback paused, pressing Play would otherwise just silently\n * re-pause it in the same synchronous tick (`toggle()`'s own check,\n * below) — no `transitionend`, no paint in between the two state\n * flips, so the button never visibly shows \"Pause\" at all before\n * reverting. Reported as looking broken, not just quiet. Disabling\n * the button while blocked (same `aria-disabled`/`tabIndex`/CSS\n * pattern as core's own slide-loading disable, Gallery.ts's\n * `setSlideLoading()`) makes \"you can't resume yet\" an honest, visible\n * state instead of a click that does nothing. Only while *not*\n * playing — while playing, the button always shows/means Pause, and\n * must stay clickable to stop regardless of zoom/rotate state.\n */\n function updateToggleAvailability(): void {\n const blocked =\n !playing && ((pauseOnZoom && zoomedIn) || (pauseOnRotateFlip && rotatedOrFlipped));\n button.ariaDisabled = blocked ? 'true' : null;\n if (blocked) button.tabIndex = -1;\n else button.removeAttribute('tabindex');\n }\n\n function setButtonState(isPlaying: boolean): void {\n playing = isPlaying;\n button.innerHTML = isPlaying ? PAUSE_ICON : PLAY_ICON;\n button.setAttribute('aria-label', isPlaying ? pauseLabel : playLabel);\n button.title = isPlaying ? pauseLabel : playLabel;\n updateToggleAvailability();\n }\n\n function onVideoEnded(): void {\n if (playing) advance();\n }\n\n // A real user pause, as opposed to the pause some browsers fire\n // immediately alongside 'ended' — `.ended` disambiguates the two.\n // Provider videos (§4.3's renderers) are responsible for holding their\n // own 'pause' dispatch back if it might just be a natural end arriving\n // slightly early — vimeo.ts's `PAUSE_DISPATCH_DELAY_MS` is why.\n function onVideoPause(): void {\n if (currentVideo?.ended) return;\n stop();\n }\n\n function detachVideo(): void {\n if (!currentVideo) return;\n currentVideo.removeEventListener('ended', onVideoEnded);\n currentVideo.removeEventListener('pause', onVideoPause);\n currentVideo = null;\n }\n\n // A provider embed (e.g. YouTube) is cross-origin — unlike native\n // <video>, its own autoplay policy requires a *direct* user gesture on\n // the embed itself, which an automatic play() arriving via this\n // timer/'ended'/slideItemLoad chain never has; it silently no-ops\n // rather than rejecting, so there's nothing to catch. Muting first is\n // what actually gets it to play — the viewer can still unmute via the\n // embed's own controls.\n //\n // A real bug, reported from real usage, found only once a second\n // promise-returning provider (Vimeo) existed to compare against:\n // re-issuing `play()`/`muted = true` on every retry — needed for\n // YouTube, whose fire-and-forget `playVideo()` can silently drop a\n // command issued before its postMessage bridge is fully settled — does\n // the opposite for a provider whose `play()` genuinely returns a\n // promise. Confirmed directly, isolated from Shoji entirely: a single,\n // uninterrupted `play()` call reliably started Vimeo playback in\n // ~1.6s, while calling it again every 400ms (this loop's original,\n // unconditional behavior) kept it stuck indefinitely — each new call\n // resets the progress the previous one had already made, so it never\n // gets an uninterrupted run long enough to actually start. (This is\n // also what point 12 below's retry-exhaustion was actually skipping\n // past: not a broken video, but this loop's own repeated calls\n // preventing it from ever finishing what the first one started.)\n // `video.play()`'s return type only *claims* to be a promise for every\n // provider (see the `PlayableMedia` doc comment above) — branching on\n // whether it genuinely is one is what lets each provider get the\n // retry behavior it actually needs from this one shared function.\n function ensureProviderPlaying(video: PlayableMedia, attemptsLeft: number): void {\n video.muted = true;\n const playResult = video.play();\n if (playResult && typeof playResult.then === 'function') {\n pollWithoutReissuing(video, attemptsLeft, playResult);\n } else {\n reissueOnEachRetry(video, attemptsLeft);\n }\n }\n\n /** Fire-and-forget `play()` (YouTube) — re-issues the command itself on every retry, per this function's own doc comment above. */\n function reissueOnEachRetry(video: PlayableMedia, attemptsLeft: number): void {\n setTimeout(() => {\n if (currentVideo !== video || !playing) return; // stale — slide changed, or already stopped\n if (!video.paused) return; // took effect\n if (attemptsLeft > 0) ensureProviderPlaying(video, attemptsLeft - 1);\n // Exhausted every retry after already muting first — muted\n // autoplay is essentially never blocked by policy, so this means\n // something is genuinely wrong with the embed, not that it's\n // waiting on a gesture. advance() (not stop()) so a slow/late\n // error report (real usage: YouTube's own error can arrive slower\n // than this retry window, e.g. Error 153) can't leave the\n // slideshow stuck if this fires first.\n else advance();\n }, PROVIDER_PLAY_RETRY_MS);\n }\n\n /** A genuine `play()` promise (e.g. Vimeo) — the command itself is only ever issued once (by the caller); this only re-checks `.paused` on the same schedule, over the same total budget, without ever calling `play()` again. */\n function pollWithoutReissuing(\n video: PlayableMedia,\n attemptsLeft: number,\n playResult: Promise<void>,\n ): void {\n // Swallowed deliberately: a rejection here isn't distinguished from\n // \"still pending\" — the .paused poll below is what actually decides\n // whether this took effect, on the same schedule regardless of how\n // the promise itself settles. An unhandled-rejection console warning\n // is the only cost, same tradeoff as the native <video> path below\n // taking a real .catch() instead when it needs to branch on *why*.\n playResult.catch(() => {});\n setTimeout(() => {\n if (currentVideo !== video || !playing) return;\n if (!video.paused) return;\n if (attemptsLeft > 0) pollWithoutReissuing(video, attemptsLeft - 1, playResult);\n else advance();\n }, PROVIDER_PLAY_RETRY_MS);\n }\n\n function enterSlide(): void {\n clearTimer();\n detachVideo();\n resetProgressBar();\n awaitingProviderVideo = false;\n if (!playing) return;\n\n const media = gallery.getActiveMedia();\n const video = findPlayable(media);\n if (video) {\n currentVideo = video;\n video.addEventListener('ended', onVideoEnded);\n video.addEventListener('pause', onVideoPause);\n if (video instanceof HTMLVideoElement) {\n const playResult = video.play();\n // A rejected play() has two different causes that look identical\n // here: the browser blocking an unmuted play() that isn't a direct\n // continuation of a user gesture (NotAllowedError — the video is\n // fine, it just needs the viewer's own click) or the video\n // genuinely being unplayable (NotSupportedError — a broken/missing\n // source, an unsupported format). Only the first pauses the\n // slideshow for the viewer to resolve by hand; anything else means\n // there's nothing to wait for, so it skips ahead instead.\n if (playResult && typeof playResult.catch === 'function') {\n playResult.catch((error: unknown) => {\n if (currentVideo !== video || !playing) return; // stale\n if (error instanceof DOMException && error.name === 'NotAllowedError') stop();\n else advance();\n });\n }\n } else {\n ensureProviderPlaying(video, MAX_PROVIDER_PLAY_ATTEMPTS);\n }\n return;\n }\n\n // A provider video (e.g. YouTube) still mid-setup isn't an ordinary\n // slide either — the slideItemLoad listener below re-enters once it's\n // actually playable. This timer is a fallback in case that never\n // happens (network failure, blocked, ...), so the slideshow can't\n // stall on it forever.\n awaitingProviderVideo = isPendingProviderVideo(media);\n runProgressBar(interval);\n timer = setTimeout(advance, interval);\n }\n\n function advance(): void {\n const before = gallery.currentIndex;\n isAdvancing = true;\n gallery.next();\n isAdvancing = false;\n // loop:false and next() was already at the last item — nothing left\n // to advance to; the 'afterSlide' handler below won't fire for a\n // no-op goTo(), so this is the only place that can catch it.\n if (gallery.currentIndex === before) stop();\n }\n\n function start(): void {\n if (playing) return;\n setButtonState(true);\n ctx.emit('autoplayStart', {});\n enterSlide();\n }\n\n function stop(): void {\n if (!playing) return;\n setButtonState(false);\n clearTimer();\n resetProgressBar();\n if (currentVideo && !currentVideo.paused) currentVideo.pause();\n detachVideo();\n ctx.emit('autoplayStop', {});\n }\n\n function toggle(): void {\n if (playing) {\n stop();\n return;\n }\n start();\n // A real bug, reported from real usage: zooming in *first*, then\n // pressing Play, never paused at all — clicking Play doesn't itself\n // fire zoomChange/rotateFlipChange, and a single-step \"toggle back to\n // neutral\" action afterward (Actual size, double-tap-to-reset) only\n // ever emits the *already-neutral* event, never one crossing the\n // engaged threshold — so nothing downstream would ever have caught it\n // either. Re-checked here instead, right after a genuine manual\n // start — the one path a viewer can actually reach a non-neutral view\n // state from *before* pressing Play. `zoomedIn`/`rotatedOrFlipped`\n // (below) track only the *current* state, purely for this check —\n // deliberately not reused for the pause-on-event logic itself, which\n // reacts to each event directly.\n if ((pauseOnZoom && zoomedIn) || (pauseOnRotateFlip && rotatedOrFlipped)) stop();\n }\n\n /**\n * Pauses the slideshow the instant a vertical drag crosses the close\n * threshold, mirroring the controls' own live hide cue\n * (`dragCloseThreshold`) — asked for directly, otherwise the\n * interval/video kept running mid-drag. Resumes only if *this*\n * crossing paused it — a manual pause (toolbar, mid-drag tap) must\n * stay paused even if the drag retreats back under the threshold.\n */\n const offDragThreshold = ctx.on('dragCloseThreshold', ({ hidden }) => {\n if (hidden) {\n wasPlayingBeforeDrag = playing;\n if (playing) stop();\n } else if (wasPlayingBeforeDrag) {\n wasPlayingBeforeDrag = false;\n start();\n }\n });\n\n /**\n * DESIGN.md §4.1 — a real UX gap, not a reported bug: nothing stopped\n * the slideshow from auto-advancing out from under a viewer actively\n * zoomed into a detail on the current slide. Only reacts to\n * `zoomChange`'s event *shape* (`core/types.ts`) — never imports Zoom\n * directly, so this is a no-op with it not loaded, or with\n * `pauseOnZoom: false` set explicitly (events over inheritance,\n * CLAUDE.md).\n *\n * Deliberately **stays paused** rather than auto-resuming once\n * un-zoomed back to neutral — the first design tried the opposite\n * (edge-tracked \"engaged\" state, auto-resume on disengage, matching\n * `dragCloseThreshold` below) and hit two real problems testing it:\n * (1) a manual restart while still zoomed left the edge-tracker stuck\n * \"already engaged,\" silently skipping the *next* re-pause; (2) once\n * fixed, un-zooming back to exactly scale 1 still auto-resumed even\n * when the viewer's very next action (confirmed directly for\n * RotateFlip's equivalent, below) was to keep interacting with the\n * view controls, not watch the slideshow. `scale > 1` still gates\n * *which* zoomChange events count (an ordinary scale-1 event can also\n * fire from an unrelated slide-change reset, not a real interaction —\n * `zoom/index.ts`'s `reset()` — so it can't unconditionally pause on\n * every event the way rotateFlipChange below safely can).\n */\n const ZOOM_ENGAGED_THRESHOLD = 1.001; // matches Zoom's own ZOOM_EPSILON — \"just barely above 1\" is float residue, not a real zoom\n const offZoomChange = ctx.on('zoomChange', ({ scale }) => {\n zoomedIn = scale > ZOOM_ENGAGED_THRESHOLD;\n if (pauseOnZoom && zoomedIn && playing) stop();\n else updateToggleAvailability(); // stop() above already refreshes this; covers zooming in while already paused, which stop() wouldn't touch\n });\n /**\n * Same UX gap, RotateFlip's own equivalent (see `pauseOnRotateFlip`'s\n * own doc comment). Pauses on *any*\n * `rotateFlipChange` event unconditionally, including one that lands\n * back on the original orientation — confirmed directly, reported from\n * real usage: rotate four times back to 0deg still reads as an active\n * interruption of the slideshow, not \"nothing happened,\" so it must\n * stay paused too, the same as landing anywhere else. Every\n * `rotateFlipChange` (unlike zoomChange above) only ever fires from a\n * real button/shortcut click — `rotateFlip/index.ts`'s own per-slide\n * `reset()` never emits it — so no extra state check is needed here.\n */\n const offRotateFlipChange = ctx.on('rotateFlipChange', ({ flipH, flipV, rotation }) => {\n rotatedOrFlipped = flipH || flipV || rotation !== 0;\n if (pauseOnRotateFlip && playing) stop();\n else updateToggleAvailability(); // stop() above already refreshes this; covers rotating while already paused, which stop() wouldn't touch\n });\n /**\n * `pauseOnCaptionExpand`'s own doc comment above — unlike the two\n * listeners above, no `updateToggleAvailability()` companion call and\n * no `zoomedIn`/`rotatedOrFlipped`-style tracked flag: the caption\n * modal already makes Play physically unreachable while `open: true`\n * (core's own focus trap + capture-phase keydown blocking, not\n * anything this plugin has to enforce), so there's no \"pressed Play\n * while still blocked\" case to disable the button for or re-check on\n * toggle() the way zoom/rotateFlip both need.\n */\n const offCaptionModalChange = ctx.on('captionModalChange', ({ open }) => {\n if (pauseOnCaptionExpand && open && playing) stop();\n });\n\n button.addEventListener('click', toggle);\n\n // 'right' — clusters immediately before the close button, per DESIGN.md §3.1.\n const removeButton = ctx.ui.toolbar('right', button);\n const removeProgress = showProgress ? ctx.ui.overlay(progress) : null;\n const removeShortcut = ctx.ui.registerShortcut(' ', toggle);\n // Detaches this plugin's own 'pause'/'ended' listeners from the outgoing\n // video before Gallery.navigate() pauses it (see Gallery.ts's comment on\n // this same event) — otherwise that pause is misread as the viewer\n // manually pausing, which stops the slideshow before the new slide's\n // afterSlide handler below ever runs. enterSlide() below also calls\n // detachVideo(), but by then it's too late for *this* transition; that\n // call is what handles the slide *after* this one instead.\n const offBeforeSlide = ctx.on('beforeSlide', () => {\n detachVideo();\n });\n // Any slide change — autoplay's own next(), or the viewer manually\n // navigating mid-slideshow via arrows/buttons/goTo() — re-enters here,\n // tearing down the previous slide's timer/video listeners and setting\n // up fresh ones for whatever is active now. advance() deliberately does\n // NOT call enterSlide() itself: next() already triggers this listener\n // synchronously, so calling it twice would double up the timer/video wiring.\n const offSlide = ctx.on('afterSlide', () => {\n // Zoom/RotateFlip both reset unanimated on navigation (DESIGN.md\n // §2.5) without re-emitting zoomChange/rotateFlipChange — resynced\n // here so a stale \"was engaged\" reading from the *outgoing* slide\n // can't wrongly re-pause toggle()'s next Play press on a slide\n // that's actually neutral now.\n zoomedIn = false;\n rotatedOrFlipped = false;\n updateToggleAvailability();\n if (!playing) return;\n // pauseOnManualNavigate (default true, see its own doc comment) —\n // isAdvancing is only ever true for the exact duration of advance()'s\n // own gallery.next() call above, so its absence here means something\n // other than autoplay itself moved the slide.\n if (pauseOnManualNavigate && !isAdvancing) stop();\n else enterSlide();\n });\n // A provider video (§4-video) that was still mid-setup when enterSlide()\n // last ran — see awaitingProviderVideo there — becomes playable some\n // time after afterSlide already fired and gave up on it for this pass.\n // Re-enter once it's genuinely ready, scoped to the still-active index\n // so a slide the viewer has already moved past doesn't retroactively\n // hijack the timer.\n const offSlideItemLoad = ctx.on('slideItemLoad', ({ index }) => {\n if (playing && awaitingProviderVideo && index === gallery.currentIndex) enterSlide();\n });\n const offClose = ctx.on('close', () => stop());\n const offOpen = ctx.on('afterOpen', () => {\n zoomedIn = false;\n rotatedOrFlipped = false;\n updateToggleAvailability();\n if (autoStart) start();\n });\n\n return () => {\n stop();\n outer.removeEventListener('error', onVideoError);\n removeButton();\n removeProgress?.();\n removeShortcut();\n offBeforeSlide();\n offSlide();\n offSlideItemLoad();\n offOpen();\n offClose();\n offDragThreshold();\n offZoomChange();\n offRotateFlipChange();\n offCaptionModalChange();\n };\n },\n};\n"],"names":[],"mappings":"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"}
package/dist/shoji.js CHANGED
@@ -3377,15 +3377,23 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
3377
3377
  const MAX_PROVIDER_PLAY_ATTEMPTS = 8;
3378
3378
  const Autoplay = {
3379
3379
  name: "autoplay",
3380
- defaults: { interval: 5e3, showProgress: true },
3380
+ defaults: {
3381
+ interval: 5e3,
3382
+ showProgress: true,
3383
+ pauseOnZoom: true,
3384
+ pauseOnRotateFlip: true,
3385
+ pauseOnCaptionExpand: true,
3386
+ pauseOnManualNavigate: true
3387
+ },
3381
3388
  init(ctx) {
3382
3389
  const { gallery } = ctx;
3383
3390
  const interval = Number(ctx.options.interval ?? 5e3);
3384
3391
  const showProgress = ctx.options.showProgress !== false;
3385
3392
  const autoStart = ctx.options.autoStart === true;
3386
- const pauseOnZoom = ctx.options.pauseOnZoom === true;
3387
- const pauseOnRotateFlip = ctx.options.pauseOnRotateFlip === true;
3388
- const pauseOnCaptionExpand = ctx.options.pauseOnCaptionExpand === true;
3393
+ const pauseOnZoom = ctx.options.pauseOnZoom !== false;
3394
+ const pauseOnRotateFlip = ctx.options.pauseOnRotateFlip !== false;
3395
+ const pauseOnCaptionExpand = ctx.options.pauseOnCaptionExpand !== false;
3396
+ const pauseOnManualNavigate = ctx.options.pauseOnManualNavigate !== false;
3389
3397
  const locale = ctx.options.locale;
3390
3398
  const playLabel = (locale == null ? void 0 : locale.play) ?? "Play slideshow";
3391
3399
  const pauseLabel = (locale == null ? void 0 : locale.pause) ?? "Pause slideshow";
@@ -3396,6 +3404,7 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
3396
3404
  let wasPlayingBeforeDrag = false;
3397
3405
  let zoomedIn = false;
3398
3406
  let rotatedOrFlipped = false;
3407
+ let isAdvancing = false;
3399
3408
  const outer = ctx.ui.outer();
3400
3409
  function onVideoError(event) {
3401
3410
  if (!playing) return;
@@ -3521,7 +3530,9 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
3521
3530
  }
3522
3531
  function advance() {
3523
3532
  const before = gallery.currentIndex;
3533
+ isAdvancing = true;
3524
3534
  gallery.next();
3535
+ isAdvancing = false;
3525
3536
  if (gallery.currentIndex === before) stop();
3526
3537
  }
3527
3538
  function start() {
@@ -3581,7 +3592,9 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
3581
3592
  zoomedIn = false;
3582
3593
  rotatedOrFlipped = false;
3583
3594
  updateToggleAvailability();
3584
- if (playing) enterSlide();
3595
+ if (!playing) return;
3596
+ if (pauseOnManualNavigate && !isAdvancing) stop();
3597
+ else enterSlide();
3585
3598
  });
3586
3599
  const offSlideItemLoad = ctx.on("slideItemLoad", ({ index }) => {
3587
3600
  if (playing && awaitingProviderVideo && index === gallery.currentIndex) enterSlide();
@@ -5220,7 +5233,7 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
5220
5233
  };
5221
5234
  }
5222
5235
  };
5223
- const version = "0.1.0-alpha.37";
5236
+ const version = "0.1.0-alpha.38";
5224
5237
  const Shoji = Object.assign(Gallery, {
5225
5238
  Autoplay,
5226
5239
  Layout,