@michaelyagi/shoji 0.1.0-alpha.10
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/LICENSE +21 -0
- package/README.md +83 -0
- package/dist/esm/core/EventBus.d.ts +16 -0
- package/dist/esm/core/FocusTrap.d.ts +9 -0
- package/dist/esm/core/Gallery.d.ts +221 -0
- package/dist/esm/core/GestureController.d.ts +57 -0
- package/dist/esm/core/LiveRegion.d.ts +6 -0
- package/dist/esm/core/SlideManager.d.ts +85 -0
- package/dist/esm/core/bodyScrollLock.d.ts +2 -0
- package/dist/esm/core/dom.d.ts +27 -0
- package/dist/esm/core/icons.d.ts +6 -0
- package/dist/esm/core/index.d.ts +5 -0
- package/dist/esm/core/index.js +2105 -0
- package/dist/esm/core/index.js.map +1 -0
- package/dist/esm/core/plugin.d.ts +54 -0
- package/dist/esm/core/rotateFlipNormalize.d.ts +19 -0
- package/dist/esm/core/scan.d.ts +19 -0
- package/dist/esm/core/types.d.ts +290 -0
- package/dist/esm/core/zoomTransition.d.ts +33 -0
- package/dist/esm/gestures/GestureEngine.d.ts +78 -0
- package/dist/esm/index.css +540 -0
- package/dist/esm/index.d.ts +32 -0
- package/dist/esm/index.js +24 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/index2.css +22 -0
- package/dist/esm/index3.css +170 -0
- package/dist/esm/index4.css +27 -0
- package/dist/esm/plugins/activeThumbnail/index.d.ts +26 -0
- package/dist/esm/plugins/activeThumbnail/index.js +63 -0
- package/dist/esm/plugins/activeThumbnail/index.js.map +1 -0
- package/dist/esm/plugins/autoplay/icons.d.ts +3 -0
- package/dist/esm/plugins/autoplay/index.d.ts +19 -0
- package/dist/esm/plugins/autoplay/index.js +207 -0
- package/dist/esm/plugins/autoplay/index.js.map +1 -0
- package/dist/esm/plugins/fullscreen/icons.d.ts +3 -0
- package/dist/esm/plugins/fullscreen/index.d.ts +17 -0
- package/dist/esm/plugins/fullscreen/index.js +74 -0
- package/dist/esm/plugins/fullscreen/index.js.map +1 -0
- package/dist/esm/plugins/layout/index.d.ts +191 -0
- package/dist/esm/plugins/layout/index.js +752 -0
- package/dist/esm/plugins/layout/index.js.map +1 -0
- package/dist/esm/plugins/layout/justified.d.ts +68 -0
- package/dist/esm/plugins/layout/masonry.d.ts +92 -0
- package/dist/esm/plugins/rotateFlip/icons.d.ts +5 -0
- package/dist/esm/plugins/rotateFlip/index.d.ts +17 -0
- package/dist/esm/plugins/rotateFlip/index.js +138 -0
- package/dist/esm/plugins/rotateFlip/index.js.map +1 -0
- package/dist/esm/plugins/video/index.d.ts +13 -0
- package/dist/esm/plugins/video/index.js +207 -0
- package/dist/esm/plugins/video/index.js.map +1 -0
- package/dist/esm/plugins/video/vimeo.d.ts +43 -0
- package/dist/esm/plugins/video/youtube.d.ts +61 -0
- package/dist/esm/plugins/zoom/icons.d.ts +4 -0
- package/dist/esm/plugins/zoom/index.d.ts +30 -0
- package/dist/esm/plugins/zoom/index.js +274 -0
- package/dist/esm/plugins/zoom/index.js.map +1 -0
- package/dist/esm/plugins/zoom/zoomMath.d.ts +24 -0
- package/dist/esm/transitions/SlideTransition.d.ts +25 -0
- package/dist/esm/transitions/presets.d.ts +21 -0
- package/dist/esm/zoomTransition-bbKHpVpA.js +110 -0
- package/dist/esm/zoomTransition-bbKHpVpA.js.map +1 -0
- package/dist/shoji.css +759 -0
- package/dist/shoji.js +3907 -0
- package/dist/shoji.js.map +1 -0
- package/dist/shoji.min.css +1 -0
- package/dist/shoji.min.js +2 -0
- package/dist/shoji.min.js.map +1 -0
- package/package.json +77 -0
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
let apiPromise$1 = null;
|
|
2
|
+
const PAUSE_DISPATCH_DELAY_MS = 250;
|
|
3
|
+
function loadVimeoApi() {
|
|
4
|
+
var _a;
|
|
5
|
+
if ((_a = window.Vimeo) == null ? void 0 : _a.Player) return Promise.resolve(window.Vimeo);
|
|
6
|
+
if (apiPromise$1) return apiPromise$1;
|
|
7
|
+
apiPromise$1 = new Promise((resolve) => {
|
|
8
|
+
const script = document.createElement("script");
|
|
9
|
+
script.src = "https://player.vimeo.com/api/player.js";
|
|
10
|
+
script.onload = () => resolve(window.Vimeo);
|
|
11
|
+
document.head.appendChild(script);
|
|
12
|
+
});
|
|
13
|
+
return apiPromise$1;
|
|
14
|
+
}
|
|
15
|
+
function wirePlayableContract$1(container, player) {
|
|
16
|
+
const playable = container;
|
|
17
|
+
playable.paused = true;
|
|
18
|
+
playable.ended = false;
|
|
19
|
+
playable.play = () => player.play();
|
|
20
|
+
playable.pause = () => player.pause();
|
|
21
|
+
let mutedState = false;
|
|
22
|
+
Object.defineProperty(playable, "muted", {
|
|
23
|
+
configurable: true,
|
|
24
|
+
get: () => mutedState,
|
|
25
|
+
set: (value) => {
|
|
26
|
+
mutedState = value;
|
|
27
|
+
player.setMuted(value);
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
return playable;
|
|
31
|
+
}
|
|
32
|
+
const renderVimeo = (container, item, onReady, signal) => {
|
|
33
|
+
var _a;
|
|
34
|
+
if (((_a = item.video) == null ? void 0 : _a.provider) !== "vimeo") return;
|
|
35
|
+
const { id, url } = item.video;
|
|
36
|
+
if (!id && !url) {
|
|
37
|
+
const placeholder = document.createElement("div");
|
|
38
|
+
placeholder.className = "shoji-slide-placeholder";
|
|
39
|
+
placeholder.textContent = "Video";
|
|
40
|
+
container.appendChild(placeholder);
|
|
41
|
+
onReady();
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
const mount = document.createElement("div");
|
|
45
|
+
mount.className = "shoji-video-mount";
|
|
46
|
+
container.appendChild(mount);
|
|
47
|
+
loadVimeoApi().then((Vimeo) => {
|
|
48
|
+
if (signal.aborted) return;
|
|
49
|
+
const playable = container;
|
|
50
|
+
const player = new Vimeo.Player(mount, {
|
|
51
|
+
...url ? { url } : { id: Number(id) },
|
|
52
|
+
playsinline: true
|
|
53
|
+
});
|
|
54
|
+
let pendingPauseTimer = null;
|
|
55
|
+
function clearPendingPause() {
|
|
56
|
+
if (pendingPauseTimer !== null) {
|
|
57
|
+
clearTimeout(pendingPauseTimer);
|
|
58
|
+
pendingPauseTimer = null;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
player.on("play", () => {
|
|
62
|
+
clearPendingPause();
|
|
63
|
+
playable.paused = false;
|
|
64
|
+
playable.dispatchEvent(new Event("play"));
|
|
65
|
+
});
|
|
66
|
+
player.on("pause", () => {
|
|
67
|
+
playable.paused = true;
|
|
68
|
+
clearPendingPause();
|
|
69
|
+
pendingPauseTimer = setTimeout(() => {
|
|
70
|
+
pendingPauseTimer = null;
|
|
71
|
+
playable.dispatchEvent(new Event("pause"));
|
|
72
|
+
}, PAUSE_DISPATCH_DELAY_MS);
|
|
73
|
+
});
|
|
74
|
+
player.on("ended", () => {
|
|
75
|
+
clearPendingPause();
|
|
76
|
+
playable.paused = true;
|
|
77
|
+
playable.ended = true;
|
|
78
|
+
playable.dispatchEvent(new Event("ended"));
|
|
79
|
+
});
|
|
80
|
+
player.on(
|
|
81
|
+
"error",
|
|
82
|
+
(error) => playable.dispatchEvent(
|
|
83
|
+
new CustomEvent("error", { bubbles: true, detail: { name: error.name } })
|
|
84
|
+
)
|
|
85
|
+
);
|
|
86
|
+
player.ready().then(() => {
|
|
87
|
+
wirePlayableContract$1(container, player);
|
|
88
|
+
onReady();
|
|
89
|
+
});
|
|
90
|
+
signal.addEventListener(
|
|
91
|
+
"abort",
|
|
92
|
+
() => {
|
|
93
|
+
clearPendingPause();
|
|
94
|
+
void player.destroy();
|
|
95
|
+
},
|
|
96
|
+
{ once: true }
|
|
97
|
+
);
|
|
98
|
+
});
|
|
99
|
+
};
|
|
100
|
+
let apiPromise = null;
|
|
101
|
+
function loadYouTubeApi() {
|
|
102
|
+
var _a;
|
|
103
|
+
if ((_a = window.YT) == null ? void 0 : _a.Player) return Promise.resolve(window.YT);
|
|
104
|
+
if (apiPromise) return apiPromise;
|
|
105
|
+
apiPromise = new Promise((resolve) => {
|
|
106
|
+
const previous = window.onYouTubeIframeAPIReady;
|
|
107
|
+
window.onYouTubeIframeAPIReady = () => {
|
|
108
|
+
previous == null ? void 0 : previous();
|
|
109
|
+
resolve(window.YT);
|
|
110
|
+
};
|
|
111
|
+
const script = document.createElement("script");
|
|
112
|
+
script.src = "https://www.youtube.com/iframe_api";
|
|
113
|
+
document.head.appendChild(script);
|
|
114
|
+
});
|
|
115
|
+
return apiPromise;
|
|
116
|
+
}
|
|
117
|
+
function wirePlayableContract(container, player) {
|
|
118
|
+
const playable = container;
|
|
119
|
+
playable.paused = true;
|
|
120
|
+
playable.ended = false;
|
|
121
|
+
playable.play = () => player.playVideo();
|
|
122
|
+
playable.pause = () => player.pauseVideo();
|
|
123
|
+
Object.defineProperty(playable, "muted", {
|
|
124
|
+
configurable: true,
|
|
125
|
+
get: () => player.isMuted(),
|
|
126
|
+
set: (value) => value ? player.mute() : player.unMute()
|
|
127
|
+
});
|
|
128
|
+
return playable;
|
|
129
|
+
}
|
|
130
|
+
function handleStateChange(event, YT, playable) {
|
|
131
|
+
if (event.data === YT.PlayerState.PLAYING) {
|
|
132
|
+
playable.paused = false;
|
|
133
|
+
playable.dispatchEvent(new Event("play"));
|
|
134
|
+
} else if (event.data === YT.PlayerState.PAUSED) {
|
|
135
|
+
playable.paused = true;
|
|
136
|
+
playable.dispatchEvent(new Event("pause"));
|
|
137
|
+
} else if (event.data === YT.PlayerState.ENDED) {
|
|
138
|
+
playable.paused = true;
|
|
139
|
+
playable.ended = true;
|
|
140
|
+
playable.dispatchEvent(new Event("ended"));
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
const renderYouTube = (container, item, onReady, signal) => {
|
|
144
|
+
var _a;
|
|
145
|
+
if (((_a = item.video) == null ? void 0 : _a.provider) !== "youtube") return;
|
|
146
|
+
const videoId = item.video.id;
|
|
147
|
+
if (!videoId) {
|
|
148
|
+
const placeholder = document.createElement("div");
|
|
149
|
+
placeholder.className = "shoji-slide-placeholder";
|
|
150
|
+
placeholder.textContent = "Video";
|
|
151
|
+
container.appendChild(placeholder);
|
|
152
|
+
onReady();
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
const mount = document.createElement("div");
|
|
156
|
+
container.appendChild(mount);
|
|
157
|
+
loadYouTubeApi().then((YT) => {
|
|
158
|
+
if (signal.aborted) return;
|
|
159
|
+
const playable = container;
|
|
160
|
+
const player = new YT.Player(mount, {
|
|
161
|
+
videoId,
|
|
162
|
+
playerVars: { playsinline: 1, rel: 0 },
|
|
163
|
+
events: {
|
|
164
|
+
// `new YT.Player()` returns before the embed's own internal bootstrap
|
|
165
|
+
// (its postMessage handshake with youtube.com) finishes — calling
|
|
166
|
+
// `player.mute()`/`playVideo()`/etc. before that completes can throw
|
|
167
|
+
// ("player.isMuted is not a function") or silently no-op, even though
|
|
168
|
+
// the constructor already returned an object. `onReady` is YouTube's
|
|
169
|
+
// own signal that the real API surface is actually callable now, so
|
|
170
|
+
// wiring the contract here — not synchronously after construction —
|
|
171
|
+
// is what makes `findPlayable()`'s "is `.play` wired?" check in
|
|
172
|
+
// Autoplay (DESIGN.md §4.1) an accurate proxy for "is it really
|
|
173
|
+
// ready?" instead of just "did we build the wrapper closures?".
|
|
174
|
+
onReady: () => {
|
|
175
|
+
wirePlayableContract(container, player);
|
|
176
|
+
onReady();
|
|
177
|
+
},
|
|
178
|
+
onStateChange: (event) => handleStateChange(event, YT, playable),
|
|
179
|
+
// bubbles: true — reaches .shoji-slide-media (the pool slot, always
|
|
180
|
+
// present regardless of readiness) without needing wirePlayableContract
|
|
181
|
+
// to have run first; onError can fire instead of onReady entirely
|
|
182
|
+
// (e.g. a removed/private video), so playable isn't a safe assumption
|
|
183
|
+
// here the way it is in onStateChange. Autoplay (DESIGN.md §4.1)
|
|
184
|
+
// listens for this to skip to the next slide instead of stalling.
|
|
185
|
+
onError: (event) => playable.dispatchEvent(
|
|
186
|
+
new CustomEvent("error", { bubbles: true, detail: { code: event.data } })
|
|
187
|
+
)
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
signal.addEventListener("abort", () => player.destroy(), { once: true });
|
|
191
|
+
});
|
|
192
|
+
};
|
|
193
|
+
const Video = {
|
|
194
|
+
name: "video",
|
|
195
|
+
init(ctx) {
|
|
196
|
+
const offYouTube = ctx.ui.registerVideoProvider("youtube", renderYouTube);
|
|
197
|
+
const offVimeo = ctx.ui.registerVideoProvider("vimeo", renderVimeo);
|
|
198
|
+
return () => {
|
|
199
|
+
offYouTube();
|
|
200
|
+
offVimeo();
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
};
|
|
204
|
+
export {
|
|
205
|
+
Video
|
|
206
|
+
};
|
|
207
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sources":["../../../../src/plugins/video/vimeo.ts","../../../../src/plugins/video/youtube.ts","../../../../src/plugins/video/index.ts"],"sourcesContent":["import type { VideoProviderRenderer } from '../../core/plugin';\n\n/**\n * The bits of Vimeo's Player SDK actually used here — not in `lib.dom.d.ts`\n * (a third-party global loaded on demand), so this is a documented interop\n * boundary (CLAUDE.md: `any` only there). Kept intentionally minimal, not a\n * full mirror of Vimeo's own typings. Unlike YouTube's IFrame API, every\n * method here is genuinely Promise-based — Vimeo has no synchronous\n * \"isMuted()\"-style query at all, which is why `wirePlayableContract` below\n * mirrors `muted` locally instead of reading it live (see its own comment).\n */\ninterface VimeoPlayer {\n play(): Promise<void>;\n pause(): Promise<void>;\n setMuted(muted: boolean): Promise<boolean>;\n ready(): Promise<void>;\n destroy(): Promise<void>;\n on(event: 'play' | 'pause' | 'ended', callback: () => void): void;\n on(event: 'error', callback: (error: VimeoErrorEvent) => void): void;\n}\n\n/** Vimeo's own error event shape (`name`/`message`, plus an undocumented `method` sometimes) — e.g. `name: 'PrivacyError'` (embedding disabled), `'PasswordError'` (password-protected). */\ninterface VimeoErrorEvent {\n name: string;\n message: string;\n}\n\ninterface VimeoNamespace {\n Player: new (\n element: HTMLElement,\n options: { id?: number; url?: string; playsinline?: boolean },\n ) => VimeoPlayer;\n}\n\ndeclare global {\n interface Window {\n Vimeo?: VimeoNamespace;\n }\n}\n\nlet apiPromise: Promise<VimeoNamespace> | null = null;\n\n// See the 'pause'/'ended' race comment where this is used, below.\nconst PAUSE_DISPATCH_DELAY_MS = 250;\n\n/**\n * Loads `https://player.vimeo.com/api/player.js` exactly once however many\n * Vimeo slides/galleries end up on a page — `window.Vimeo` is a single\n * global. Simpler than `youtube.ts`'s `loadYouTubeApi`: Vimeo's script has\n * no global-callback side channel to compose with (nothing else could\n * already be watching for it the way `onYouTubeIframeAPIReady` can be), so\n * a plain `script.onload` is enough.\n */\nfunction loadVimeoApi(): Promise<VimeoNamespace> {\n if (window.Vimeo?.Player) return Promise.resolve(window.Vimeo);\n if (apiPromise) return apiPromise;\n\n apiPromise = new Promise((resolve) => {\n const script = document.createElement('script');\n script.src = 'https://player.vimeo.com/api/player.js';\n script.onload = () => resolve(window.Vimeo!);\n document.head.appendChild(script);\n });\n return apiPromise;\n}\n\n/**\n * DESIGN.md §4-autoplay — Autoplay looks for exactly this shape (`.play`/\n * `.pause`/`.paused`/`.ended`, plus real `play`/`pause`/`ended` DOM events)\n * on whatever's active, native `<video>` or not. Augmenting `container` this\n * way is what makes slideshow sync work without Autoplay ever having to\n * know Vimeo — or any other provider — exists. `play` genuinely returns\n * `player.play()`'s own promise (unlike `youtube.ts`'s fire-and-forget\n * `.play`) — Autoplay's own `ensureProviderPlaying` (§4.1 point 12) branches\n * its retry strategy on whether this is a real thenable at runtime.\n */\ntype PlayableElement = HTMLElement & {\n play: () => Promise<void>;\n pause: () => void;\n paused: boolean;\n ended: boolean;\n muted: boolean;\n};\n\n/**\n * `muted`'s setter genuinely forwards to `player.setMuted()` (fire-and-\n * forget, same as `youtube.ts`'s `mute()`/`unMute()` calls), but the getter\n * mirrors a locally-tracked value instead of reading the player live —\n * unlike YouTube's `isMuted()`, Vimeo's `getMuted()` is Promise-based, so\n * there's no synchronous value to read at all. Harmless here: nothing in\n * Autoplay ever reads `.muted` back, only sets it (DESIGN.md §4.1 point 8's\n * `video.muted = true` before an automatic play).\n */\nfunction wirePlayableContract(container: HTMLElement, player: VimeoPlayer): PlayableElement {\n const playable = container as PlayableElement;\n playable.paused = true;\n playable.ended = false;\n playable.play = () => player.play();\n playable.pause = () => player.pause();\n let mutedState = false;\n Object.defineProperty(playable, 'muted', {\n configurable: true,\n get: () => mutedState,\n set: (value: boolean) => {\n mutedState = value;\n player.setMuted(value);\n },\n });\n return playable;\n}\n\n/**\n * DESIGN.md §4-video — no poster/thumbnail handling at all: the slide shows\n * nothing until the embed itself is ready (same spinner-then-reveal every\n * other slide type gets), never an auto-fetched or guessed preview image.\n */\nexport const renderVimeo: VideoProviderRenderer = (container, item, onReady, signal) => {\n if (item.video?.provider !== 'vimeo') return;\n const { id, url } = item.video;\n if (!id && !url) {\n // A misconfigured item — scan.ts already warned about this at\n // normalization time. Same placeholder SlideManager itself uses for \"no\n // source\"/\"provider not registered\", so the spinner doesn't hang\n // forever waiting for a ready() that would never resolve otherwise.\n const placeholder = document.createElement('div');\n placeholder.className = 'shoji-slide-placeholder';\n placeholder.textContent = 'Video';\n container.appendChild(placeholder);\n onReady();\n return;\n }\n\n // shoji-video-mount (styles/shoji.css) — several real bugs, reported from\n // real usage, chased through in sequence:\n //\n // 1. Left at its own defaults, Vimeo's SDK sizes `mount` to a small fixed\n // box (its own embed default, ~640x360) via inline styles — CSS's own\n // `width/height: 100%` on the iframe only ever resolves relative to\n // that fixed-size `mount`, not the actual slide, so the embed rendered\n // small, in a corner, instead of filling the dialog.\n //\n // 2. `responsive: true` looked like the fix (and does make `mount` fill\n // its parent's *width*) but doesn't actually solve this: it computes\n // `mount`'s *height* from that width times the video's own native\n // aspect ratio via a padding-percentage trick, entirely ignoring how\n // much vertical space the slide actually has — confirmed directly at a\n // wide-but-short viewport, the resulting iframe came out taller than\n // its container, pushing the bottom of the video (and Vimeo's own\n // scrubber/controls bar, always along that same edge) below the\n // visible area.\n //\n // 3. Fully stretching `mount` to the container's own box instead (no\n // aspect-ratio at all) fixed that, but traded it for a third bug:\n // stretching regardless of the video's real 16:9 shape exposed\n // Vimeo's own internal letterbox color (white — not overridable from\n // here, that content is cross-origin) wherever the box wasn't already\n // 16:9 itself, instead of Shoji's own dark backdrop.\n //\n // `shoji-video-mount`'s CSS rules are what actually solve all three at\n // once now: contained to a real 16:9 box via container query units\n // (`!important` still doing the \"discard whatever Vimeo tries to set\n // inline\" job point 1 needed), so any leftover gap is *outside* the\n // iframe, showing this element's own transparent background — Shoji's\n // dark backdrop — rather than the provider's white. YouTube's plain\n // iframe (no such wrapper) gets the identical treatment directly, see\n // `.shoji-slide-provider-video > iframe` in shoji.css.\n const mount = document.createElement('div');\n mount.className = 'shoji-video-mount';\n container.appendChild(mount);\n\n loadVimeoApi().then((Vimeo) => {\n if (signal.aborted) return; // navigated away before the (possibly first-ever, slow) API load resolved\n\n const playable = container as PlayableElement;\n // `url` (the original data-shoji-video/video.url, when present) is\n // preferred over a bare id: it's what carries an unlisted video's\n // privacy hash (vimeo.com/{id}/{hash}) through to the player, which a\n // numeric id alone can't express. Only dynamic-mode items authored\n // without a url ever fall back to id-only.\n const player = new Vimeo.Player(mount, {\n ...(url ? { url } : { id: Number(id) }),\n playsinline: true,\n });\n\n // A real bug, reported from real usage: reaching the end of the video\n // stopped the slideshow instead of advancing it, and — the tell —\n // manually pressing play again restarted the same video from 0 instead\n // of resuming anywhere. Confirmed directly: Vimeo's own 'pause' arrives\n // as a genuinely separate postMessage event shortly *before* 'ended'\n // (~20ms apart in testing) when a video reaches its natural end, unlike\n // native `<video>`, whose `.ended` property the browser sets\n // synchronously before firing either event — Autoplay's own\n // `onVideoPause` (DESIGN.md §4.1 point 3) checks `.ended` to tell a\n // real pause from an end-adjacent one, and that check reliably lost the\n // race, reading `false` and treating a natural end as a manual pause.\n // Fixed by not dispatching 'pause' immediately: it's held for\n // `PAUSE_DISPATCH_DELAY_MS`, comfortably above the observed gap, and\n // dropped entirely if 'ended' arrives first — which is also then the\n // *only* event a natural end ever produces, matching how a single\n // native `<video>` 'pause' alongside 'ended' was already a non-issue\n // via the `.ended` check alone. A genuine manual pause (no 'ended'\n // following) still dispatches, just delayed by that same short window\n // — imperceptible for a real pause, and worth it for a correct one.\n let pendingPauseTimer: ReturnType<typeof setTimeout> | null = null;\n function clearPendingPause(): void {\n if (pendingPauseTimer !== null) {\n clearTimeout(pendingPauseTimer);\n pendingPauseTimer = null;\n }\n }\n player.on('play', () => {\n clearPendingPause();\n playable.paused = false;\n playable.dispatchEvent(new Event('play'));\n });\n player.on('pause', () => {\n playable.paused = true;\n clearPendingPause();\n pendingPauseTimer = setTimeout(() => {\n pendingPauseTimer = null;\n playable.dispatchEvent(new Event('pause'));\n }, PAUSE_DISPATCH_DELAY_MS);\n });\n player.on('ended', () => {\n clearPendingPause();\n playable.paused = true;\n playable.ended = true;\n playable.dispatchEvent(new Event('ended'));\n });\n // bubbles: true — reaches .shoji-slide-media (the pool slot, always\n // present regardless of readiness) without needing wirePlayableContract\n // to have run first; matches youtube.ts's onError — a player can error\n // out (bad id, privacy-restricted, password-protected) without ever\n // becoming ready. Autoplay (DESIGN.md §4.1) listens for this to skip to\n // the next slide instead of stalling.\n player.on('error', (error) =>\n playable.dispatchEvent(\n new CustomEvent('error', { bubbles: true, detail: { name: error.name } }),\n ),\n );\n\n player.ready().then(() => {\n wirePlayableContract(container, player);\n onReady();\n });\n\n signal.addEventListener(\n 'abort',\n () => {\n clearPendingPause();\n void player.destroy();\n },\n { once: true },\n );\n });\n};\n","import type { VideoProviderRenderer } from '../../core/plugin';\n\n/**\n * The bits of the YouTube IFrame Player API actually used here — not in\n * `lib.dom.d.ts` (a third-party global loaded on demand), so this is a\n * documented interop boundary (CLAUDE.md: `any` only there). Kept\n * intentionally minimal, not a full mirror of YouTube's own typings.\n */\ninterface YTPlayer {\n playVideo(): void;\n pauseVideo(): void;\n mute(): void;\n unMute(): void;\n isMuted(): boolean;\n destroy(): void;\n}\n\ninterface YTPlayerStateEvent {\n data: number;\n}\n\n/**\n * Same `{ data: number }` shape as `YTPlayerStateEvent` — `data` holds an\n * error code there (2 invalid parameter, 5 HTML5 player error, 100 video\n * not found/private, 101/150 embedding disabled by the owner; undocumented\n * codes like 153 are also reported in the wild) rather than a player state.\n * Named separately from `YTPlayerStateEvent` for readability at the call\n * site, not because the shape actually differs.\n */\ninterface YTPlayerErrorEvent {\n data: number;\n}\n\ninterface YTNamespace {\n Player: new (\n el: HTMLElement,\n opts: {\n videoId: string;\n playerVars?: { playsinline?: 0 | 1; rel?: 0 | 1 };\n events?: {\n onReady?: () => void;\n onStateChange?: (event: YTPlayerStateEvent) => void;\n onError?: (event: YTPlayerErrorEvent) => void;\n };\n },\n ) => YTPlayer;\n PlayerState: { ENDED: number; PLAYING: number; PAUSED: number };\n}\n\ndeclare global {\n interface Window {\n YT?: YTNamespace;\n onYouTubeIframeAPIReady?: () => void;\n }\n}\n\nlet apiPromise: Promise<YTNamespace> | null = null;\n\n/**\n * Loads `https://www.youtube.com/iframe_api` exactly once however many\n * YouTube slides/galleries end up on a page — the API itself is a single\n * global (`window.YT`), and re-injecting the script tag would be wasteful\n * and risks a second, conflicting `onYouTubeIframeAPIReady` firing. Composes\n * with a host's own `onYouTubeIframeAPIReady`, if one already exists\n * (e.g. the host embeds YouTube elsewhere too), instead of clobbering it.\n */\nfunction loadYouTubeApi(): Promise<YTNamespace> {\n if (window.YT?.Player) return Promise.resolve(window.YT);\n if (apiPromise) return apiPromise;\n\n apiPromise = new Promise((resolve) => {\n const previous = window.onYouTubeIframeAPIReady;\n window.onYouTubeIframeAPIReady = () => {\n previous?.();\n resolve(window.YT!);\n };\n const script = document.createElement('script');\n script.src = 'https://www.youtube.com/iframe_api';\n document.head.appendChild(script);\n });\n return apiPromise;\n}\n\n/**\n * DESIGN.md §4-autoplay — Autoplay looks for exactly this shape (`.play`/\n * `.pause`/`.paused`/`.ended`, plus real `play`/`pause`/`ended` DOM events)\n * on whatever's active, native `<video>` or not. Augmenting `container`\n * this way is what makes slideshow sync work without Autoplay ever having\n * to know YouTube — or any other provider — exists.\n */\ntype PlayableElement = HTMLElement & {\n play: () => void;\n pause: () => void;\n paused: boolean;\n ended: boolean;\n muted: boolean;\n};\n\n/**\n * `muted` is a real accessor (not a plain property) backed by the player's\n * own `mute()`/`unMute()`/`isMuted()` — DESIGN.md §4.1's autoplay-policy fix\n * sets `video.muted = true` right before an *automatic* play, the same way\n * it would for a native `<video>`; a plain property here would just be\n * inert state that never actually silences the embed.\n */\nfunction wirePlayableContract(container: HTMLElement, player: YTPlayer): PlayableElement {\n const playable = container as PlayableElement;\n playable.paused = true;\n playable.ended = false;\n playable.play = () => player.playVideo();\n playable.pause = () => player.pauseVideo();\n Object.defineProperty(playable, 'muted', {\n configurable: true,\n get: () => player.isMuted(),\n set: (value: boolean) => (value ? player.mute() : player.unMute()),\n });\n return playable;\n}\n\nfunction handleStateChange(\n event: YTPlayerStateEvent,\n YT: YTNamespace,\n playable: PlayableElement,\n): void {\n if (event.data === YT.PlayerState.PLAYING) {\n playable.paused = false;\n playable.dispatchEvent(new Event('play'));\n } else if (event.data === YT.PlayerState.PAUSED) {\n playable.paused = true;\n playable.dispatchEvent(new Event('pause'));\n } else if (event.data === YT.PlayerState.ENDED) {\n playable.paused = true;\n playable.ended = true;\n playable.dispatchEvent(new Event('ended'));\n }\n}\n\n/**\n * DESIGN.md §4-video — no poster/thumbnail handling at all: the slide shows\n * nothing until the embed itself is ready (same spinner-then-reveal every\n * other slide type gets), never an auto-fetched or guessed preview image.\n */\nexport const renderYouTube: VideoProviderRenderer = (container, item, onReady, signal) => {\n if (item.video?.provider !== 'youtube') return;\n const videoId = item.video.id;\n if (!videoId) {\n // A misconfigured item — scan.ts (data-shoji-video-id / video: true /\n // video.id auto-fill) already warned about this at normalization time.\n // Same placeholder SlideManager itself uses for \"no source\"/\"provider\n // not registered\", so the spinner doesn't hang forever waiting for an\n // onReady that would never fire otherwise.\n const placeholder = document.createElement('div');\n placeholder.className = 'shoji-slide-placeholder';\n placeholder.textContent = 'Video';\n container.appendChild(placeholder);\n onReady();\n return;\n }\n\n const mount = document.createElement('div');\n container.appendChild(mount);\n\n loadYouTubeApi().then((YT) => {\n if (signal.aborted) return; // navigated away before the (possibly first-ever, slow) API load resolved\n\n const playable = container as PlayableElement;\n const player = new YT.Player(mount, {\n videoId,\n playerVars: { playsinline: 1, rel: 0 },\n events: {\n // `new YT.Player()` returns before the embed's own internal bootstrap\n // (its postMessage handshake with youtube.com) finishes — calling\n // `player.mute()`/`playVideo()`/etc. before that completes can throw\n // (\"player.isMuted is not a function\") or silently no-op, even though\n // the constructor already returned an object. `onReady` is YouTube's\n // own signal that the real API surface is actually callable now, so\n // wiring the contract here — not synchronously after construction —\n // is what makes `findPlayable()`'s \"is `.play` wired?\" check in\n // Autoplay (DESIGN.md §4.1) an accurate proxy for \"is it really\n // ready?\" instead of just \"did we build the wrapper closures?\".\n onReady: () => {\n wirePlayableContract(container, player);\n onReady();\n },\n onStateChange: (event) => handleStateChange(event, YT, playable),\n // bubbles: true — reaches .shoji-slide-media (the pool slot, always\n // present regardless of readiness) without needing wirePlayableContract\n // to have run first; onError can fire instead of onReady entirely\n // (e.g. a removed/private video), so playable isn't a safe assumption\n // here the way it is in onStateChange. Autoplay (DESIGN.md §4.1)\n // listens for this to skip to the next slide instead of stalling.\n onError: (event) =>\n playable.dispatchEvent(\n new CustomEvent('error', { bubbles: true, detail: { code: event.data } }),\n ),\n },\n });\n signal.addEventListener('abort', () => player.destroy(), { once: true });\n });\n};\n","import type { PluginContext, ShojiPlugin } from '../../core/plugin';\nimport { renderVimeo } from './vimeo';\nimport { renderYouTube } from './youtube';\n\n/**\n * DESIGN.md §4-video — renders `{ video: { provider: 'youtube' | 'vimeo', id } }`\n * items (produced by `scan.ts`'s `data-shoji-video` host detection, or\n * authored directly in dynamic mode) as a real embed instead of\n * `SlideManager`'s native-`<video>` fallback, which can't play either URL at\n * all. Purely a renderer: no options, no toolbar button, no poster/\n * thumbnail handling — the slide shows nothing until the embed itself is\n * ready, same as every other slide type. Autoplay (§4-autoplay) picks up\n * real play/pause/ended sync automatically; see `youtube.ts`'s/`vimeo.ts`'s\n * `wirePlayableContract`.\n */\nexport const Video: ShojiPlugin = {\n name: 'video',\n\n init(ctx: PluginContext): () => void {\n const offYouTube = ctx.ui.registerVideoProvider('youtube', renderYouTube);\n const offVimeo = ctx.ui.registerVideoProvider('vimeo', renderVimeo);\n return () => {\n offYouTube();\n offVimeo();\n };\n },\n};\n"],"names":["apiPromise","wirePlayableContract"],"mappings":"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"}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { VideoProviderRenderer } from '../../core/plugin';
|
|
2
|
+
/**
|
|
3
|
+
* The bits of Vimeo's Player SDK actually used here — not in `lib.dom.d.ts`
|
|
4
|
+
* (a third-party global loaded on demand), so this is a documented interop
|
|
5
|
+
* boundary (CLAUDE.md: `any` only there). Kept intentionally minimal, not a
|
|
6
|
+
* full mirror of Vimeo's own typings. Unlike YouTube's IFrame API, every
|
|
7
|
+
* method here is genuinely Promise-based — Vimeo has no synchronous
|
|
8
|
+
* "isMuted()"-style query at all, which is why `wirePlayableContract` below
|
|
9
|
+
* mirrors `muted` locally instead of reading it live (see its own comment).
|
|
10
|
+
*/
|
|
11
|
+
interface VimeoPlayer {
|
|
12
|
+
play(): Promise<void>;
|
|
13
|
+
pause(): Promise<void>;
|
|
14
|
+
setMuted(muted: boolean): Promise<boolean>;
|
|
15
|
+
ready(): Promise<void>;
|
|
16
|
+
destroy(): Promise<void>;
|
|
17
|
+
on(event: 'play' | 'pause' | 'ended', callback: () => void): void;
|
|
18
|
+
on(event: 'error', callback: (error: VimeoErrorEvent) => void): void;
|
|
19
|
+
}
|
|
20
|
+
/** Vimeo's own error event shape (`name`/`message`, plus an undocumented `method` sometimes) — e.g. `name: 'PrivacyError'` (embedding disabled), `'PasswordError'` (password-protected). */
|
|
21
|
+
interface VimeoErrorEvent {
|
|
22
|
+
name: string;
|
|
23
|
+
message: string;
|
|
24
|
+
}
|
|
25
|
+
interface VimeoNamespace {
|
|
26
|
+
Player: new (element: HTMLElement, options: {
|
|
27
|
+
id?: number;
|
|
28
|
+
url?: string;
|
|
29
|
+
playsinline?: boolean;
|
|
30
|
+
}) => VimeoPlayer;
|
|
31
|
+
}
|
|
32
|
+
declare global {
|
|
33
|
+
interface Window {
|
|
34
|
+
Vimeo?: VimeoNamespace;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* DESIGN.md §4-video — no poster/thumbnail handling at all: the slide shows
|
|
39
|
+
* nothing until the embed itself is ready (same spinner-then-reveal every
|
|
40
|
+
* other slide type gets), never an auto-fetched or guessed preview image.
|
|
41
|
+
*/
|
|
42
|
+
export declare const renderVimeo: VideoProviderRenderer;
|
|
43
|
+
export {};
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { VideoProviderRenderer } from '../../core/plugin';
|
|
2
|
+
/**
|
|
3
|
+
* The bits of the YouTube IFrame Player API actually used here — not in
|
|
4
|
+
* `lib.dom.d.ts` (a third-party global loaded on demand), so this is a
|
|
5
|
+
* documented interop boundary (CLAUDE.md: `any` only there). Kept
|
|
6
|
+
* intentionally minimal, not a full mirror of YouTube's own typings.
|
|
7
|
+
*/
|
|
8
|
+
interface YTPlayer {
|
|
9
|
+
playVideo(): void;
|
|
10
|
+
pauseVideo(): void;
|
|
11
|
+
mute(): void;
|
|
12
|
+
unMute(): void;
|
|
13
|
+
isMuted(): boolean;
|
|
14
|
+
destroy(): void;
|
|
15
|
+
}
|
|
16
|
+
interface YTPlayerStateEvent {
|
|
17
|
+
data: number;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Same `{ data: number }` shape as `YTPlayerStateEvent` — `data` holds an
|
|
21
|
+
* error code there (2 invalid parameter, 5 HTML5 player error, 100 video
|
|
22
|
+
* not found/private, 101/150 embedding disabled by the owner; undocumented
|
|
23
|
+
* codes like 153 are also reported in the wild) rather than a player state.
|
|
24
|
+
* Named separately from `YTPlayerStateEvent` for readability at the call
|
|
25
|
+
* site, not because the shape actually differs.
|
|
26
|
+
*/
|
|
27
|
+
interface YTPlayerErrorEvent {
|
|
28
|
+
data: number;
|
|
29
|
+
}
|
|
30
|
+
interface YTNamespace {
|
|
31
|
+
Player: new (el: HTMLElement, opts: {
|
|
32
|
+
videoId: string;
|
|
33
|
+
playerVars?: {
|
|
34
|
+
playsinline?: 0 | 1;
|
|
35
|
+
rel?: 0 | 1;
|
|
36
|
+
};
|
|
37
|
+
events?: {
|
|
38
|
+
onReady?: () => void;
|
|
39
|
+
onStateChange?: (event: YTPlayerStateEvent) => void;
|
|
40
|
+
onError?: (event: YTPlayerErrorEvent) => void;
|
|
41
|
+
};
|
|
42
|
+
}) => YTPlayer;
|
|
43
|
+
PlayerState: {
|
|
44
|
+
ENDED: number;
|
|
45
|
+
PLAYING: number;
|
|
46
|
+
PAUSED: number;
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
declare global {
|
|
50
|
+
interface Window {
|
|
51
|
+
YT?: YTNamespace;
|
|
52
|
+
onYouTubeIframeAPIReady?: () => void;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* DESIGN.md §4-video — no poster/thumbnail handling at all: the slide shows
|
|
57
|
+
* nothing until the embed itself is ready (same spinner-then-reveal every
|
|
58
|
+
* other slide type gets), never an auto-fetched or guessed preview image.
|
|
59
|
+
*/
|
|
60
|
+
export declare const renderYouTube: VideoProviderRenderer;
|
|
61
|
+
export {};
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
/** DESIGN.md §9 — inline SVG, stroke = currentColor, matches src/core/icons.ts's convention. A magnifying glass with a +/− in the lens, and a plain expand-corners glyph for "actual size" (distinct from fullscreen's EXPAND_ICON — no diagonal corner arrows, just a frame, so the two aren't visually confusable when both plugins are enabled). */
|
|
2
|
+
export declare const ZOOM_IN_ICON = "<svg viewBox=\"0 0 24 24\" width=\"24\" height=\"24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><circle cx=\"10\" cy=\"10\" r=\"7\"/><path d=\"M21 21l-5.5-5.5M10 7v6M7 10h6\"/></svg>";
|
|
3
|
+
export declare const ZOOM_OUT_ICON = "<svg viewBox=\"0 0 24 24\" width=\"24\" height=\"24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><circle cx=\"10\" cy=\"10\" r=\"7\"/><path d=\"M21 21l-5.5-5.5M7 10h6\"/></svg>";
|
|
4
|
+
export declare const ZOOM_ACTUAL_SIZE_ICON = "<svg viewBox=\"0 0 24 24\" width=\"24\" height=\"24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><rect x=\"4\" y=\"4\" width=\"16\" height=\"16\" rx=\"1\"/><path d=\"M9 15l6-6M9 9h0M15 15h0\"/></svg>";
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { ShojiPlugin } from '../../core/plugin';
|
|
2
|
+
export interface ZoomOptions {
|
|
3
|
+
/** Multiplier cap for pinch/wheel/button zoom. "Actual size" can exceed this deliberately — it's an explicit action, not continuous gesture zoom. Default 4. */
|
|
4
|
+
maxScale?: number;
|
|
5
|
+
/** Scale a double-tap/double-click jumps to; a second one while already zoomed resets to 1 instead. Default 2. */
|
|
6
|
+
doubleTapScale?: number;
|
|
7
|
+
/** Multiplier applied per zoom-in/zoom-out toolbar button click. Default 1.5. */
|
|
8
|
+
buttonStep?: number;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* DESIGN.md §4-zoom — pinch, double-tap/click, wheel+ctrl, and three toolbar
|
|
12
|
+
* buttons (zoom in/out/actual-size) all drive a single `scale`/pan state on
|
|
13
|
+
* the active slide's `<img>` — never `.shoji-slide-media` itself, which the
|
|
14
|
+
* rotateFlip plugin (§4) already transforms; nesting on the inner element
|
|
15
|
+
* instead of fighting over the same transform string means a rotated *and*
|
|
16
|
+
* zoomed photo behaves correctly for free (the outer rotate carries the
|
|
17
|
+
* inner pan/scale along with it as a rigid unit, which is also the visually
|
|
18
|
+
* expected result — see DESIGN.md's note on this plugin for the full
|
|
19
|
+
* reasoning). Pinch/double-tap/wheel are core's own gesture relay (§2.4) —
|
|
20
|
+
* scaffolding that existed specifically for this plugin to consume, not
|
|
21
|
+
* reimplemented here. Pan (single-pointer drag while zoomed) is the one
|
|
22
|
+
* piece core's relay doesn't cover — core's own drag-to-navigate/
|
|
23
|
+
* drag-to-close would otherwise fight over the same drag — so this plugin
|
|
24
|
+
* registers a zoom gate (`Gallery.registerZoomGate`, §4-zoom) that suspends
|
|
25
|
+
* core's drag handling entirely while zoomed, and tracks pan with its own
|
|
26
|
+
* minimal raw pointer listeners instead of reusing `GestureEngine` (whose
|
|
27
|
+
* axis-locked model — pick horizontal *or* vertical per gesture — is the
|
|
28
|
+
* wrong shape for a 2D pan that needs both at once).
|
|
29
|
+
*/
|
|
30
|
+
export declare const Zoom: ShojiPlugin;
|