@mango-iiif/av 0.3.0 → 1.0.0

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.
Files changed (69) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/README.md +25 -13
  3. package/dist/annotations.d.ts +2 -2
  4. package/dist/annotations.js +4 -4
  5. package/dist/captions.d.ts +23 -0
  6. package/dist/captions.js +16 -0
  7. package/dist/captions.js.map +1 -0
  8. package/dist/chapters.d.ts +2 -2
  9. package/dist/chapters.js +3 -3
  10. package/dist/{chunk-4NOWGMI5.js → chunk-53ZN4Y4L.js} +28 -66
  11. package/dist/chunk-53ZN4Y4L.js.map +1 -0
  12. package/dist/chunk-AWLDINQR.js +73 -0
  13. package/dist/chunk-AWLDINQR.js.map +1 -0
  14. package/dist/chunk-BB7RE7EO.js +407 -0
  15. package/dist/chunk-BB7RE7EO.js.map +1 -0
  16. package/dist/chunk-BMMQFCD3.js +86 -0
  17. package/dist/chunk-BMMQFCD3.js.map +1 -0
  18. package/dist/chunk-IZYEGSGT.js +148 -0
  19. package/dist/chunk-IZYEGSGT.js.map +1 -0
  20. package/dist/chunk-JXZR5DL2.js +160 -0
  21. package/dist/chunk-JXZR5DL2.js.map +1 -0
  22. package/dist/{chunk-CIAQZ2EB.js → chunk-LMZIFVHN.js} +8 -4
  23. package/dist/chunk-LMZIFVHN.js.map +1 -0
  24. package/dist/chunk-TQNEVWUH.js +39 -0
  25. package/dist/chunk-TQNEVWUH.js.map +1 -0
  26. package/dist/chunk-XXM4G3K5.js +296 -0
  27. package/dist/chunk-XXM4G3K5.js.map +1 -0
  28. package/dist/chunk-YVI7J43Q.js +29 -0
  29. package/dist/chunk-YVI7J43Q.js.map +1 -0
  30. package/dist/chunk-Z2OF3RKT.js +152 -0
  31. package/dist/chunk-Z2OF3RKT.js.map +1 -0
  32. package/dist/{controller-SkwGkVE8.d.ts → controller-Ag3nFtwP.d.ts} +16 -24
  33. package/dist/{controller-element-CkXIA6w2.d.ts → controller-element-Cn5cxKnD.d.ts} +4 -2
  34. package/dist/core.d.ts +2 -2
  35. package/dist/core.js +2 -2
  36. package/dist/index.d.ts +6 -3
  37. package/dist/index.js +21 -14
  38. package/dist/index.js.map +1 -1
  39. package/dist/media.d.ts +3 -3
  40. package/dist/media.js +11 -5
  41. package/dist/media.js.map +1 -1
  42. package/dist/player.d.ts +8 -2
  43. package/dist/player.js +3 -5
  44. package/dist/player.js.map +1 -1
  45. package/dist/ramp-2XKALEGA.js +98 -0
  46. package/dist/ramp-2XKALEGA.js.map +1 -0
  47. package/dist/transcript-element.d.ts +2 -2
  48. package/dist/transcript-element.js +6 -5
  49. package/dist/transcript-element.js.map +1 -1
  50. package/docs/API.md +70 -32
  51. package/package.json +18 -11
  52. package/dist/chunk-4NOWGMI5.js.map +0 -1
  53. package/dist/chunk-4V6FME6B.js +0 -643
  54. package/dist/chunk-4V6FME6B.js.map +0 -1
  55. package/dist/chunk-C4H46HDS.js +0 -95
  56. package/dist/chunk-C4H46HDS.js.map +0 -1
  57. package/dist/chunk-CDLYT3KB.js +0 -17
  58. package/dist/chunk-CDLYT3KB.js.map +0 -1
  59. package/dist/chunk-CIAQZ2EB.js.map +0 -1
  60. package/dist/chunk-EHIKFRFV.js +0 -75
  61. package/dist/chunk-EHIKFRFV.js.map +0 -1
  62. package/dist/chunk-ELGGWWDS.js +0 -257
  63. package/dist/chunk-ELGGWWDS.js.map +0 -1
  64. package/dist/chunk-FVYEV2LO.js +0 -163
  65. package/dist/chunk-FVYEV2LO.js.map +0 -1
  66. package/dist/chunk-YX32ZQPR.js +0 -83
  67. package/dist/chunk-YX32ZQPR.js.map +0 -1
  68. package/dist/chunk-YX6L6CUF.js +0 -355
  69. package/dist/chunk-YX6L6CUF.js.map +0 -1
@@ -0,0 +1,407 @@
1
+ import {
2
+ AVControllerElement,
3
+ componentTheme
4
+ } from "./chunk-JXZR5DL2.js";
5
+ import {
6
+ escapeHtml,
7
+ formatString,
8
+ resolveStrings
9
+ } from "./chunk-53ZN4Y4L.js";
10
+
11
+ // src/components/ramp-manifest.ts
12
+ var text = (value) => ({ none: [value] });
13
+ var renderingItems = (files) => files.map((file) => ({
14
+ id: file.id,
15
+ type: "Text",
16
+ label: text(file.label),
17
+ ...file.format ? { format: file.format } : {}
18
+ }));
19
+ var rampManifest = (manifest, canvas, source, options) => {
20
+ const duration = canvas.duration ?? source.duration;
21
+ const tracks = options.captions ? canvas.tracks.filter((track) => track.kind === "captions" || track.kind === "subtitles") : [];
22
+ const body = {
23
+ id: options.src,
24
+ type: source.kind === "audio" ? "Sound" : "Video",
25
+ ...source.format ? { format: source.format } : {},
26
+ ...source.width ? { width: source.width } : {},
27
+ ...source.height ? { height: source.height } : {},
28
+ ...duration ? { duration } : {}
29
+ };
30
+ const rampCanvas = {
31
+ id: canvas.id,
32
+ type: "Canvas",
33
+ label: text(canvas.label),
34
+ ...duration ? { duration } : {},
35
+ ...canvas.width ? { width: canvas.width } : {},
36
+ ...canvas.height ? { height: canvas.height } : {},
37
+ items: [{
38
+ id: `${canvas.id}/ramp/page`,
39
+ type: "AnnotationPage",
40
+ items: [{
41
+ id: `${canvas.id}/ramp/painting`,
42
+ type: "Annotation",
43
+ motivation: "painting",
44
+ body,
45
+ target: canvas.id
46
+ }]
47
+ }]
48
+ };
49
+ if (tracks.length > 0) {
50
+ rampCanvas.annotations = [{
51
+ id: `${canvas.id}/ramp/captions`,
52
+ type: "AnnotationPage",
53
+ items: tracks.map((track, index) => ({
54
+ id: `${canvas.id}/ramp/captions/${index}`,
55
+ type: "Annotation",
56
+ motivation: "supplementing",
57
+ body: {
58
+ id: track.src,
59
+ type: "Text",
60
+ // RAMP reads the format to choose the track kind, and fails without one.
61
+ format: track.format ?? "text/vtt",
62
+ label: text(track.label),
63
+ ...track.language ? { language: track.language } : {}
64
+ },
65
+ target: canvas.id
66
+ }))
67
+ }];
68
+ }
69
+ if (canvas.poster && source.kind === "video") {
70
+ rampCanvas.placeholderCanvas = {
71
+ id: `${canvas.id}/ramp/poster`,
72
+ type: "Canvas",
73
+ items: [{
74
+ id: `${canvas.id}/ramp/poster/page`,
75
+ type: "AnnotationPage",
76
+ items: [{
77
+ id: `${canvas.id}/ramp/poster/painting`,
78
+ type: "Annotation",
79
+ motivation: "painting",
80
+ body: { id: canvas.poster, type: "Image" },
81
+ target: `${canvas.id}/ramp/poster`
82
+ }]
83
+ }]
84
+ };
85
+ }
86
+ if (options.download && canvas.rendering.length > 0) rampCanvas.rendering = renderingItems(canvas.rendering);
87
+ return {
88
+ "@context": "http://iiif.io/api/presentation/3/context.json",
89
+ id: manifest.id,
90
+ type: "Manifest",
91
+ label: text(manifest.label),
92
+ items: [rampCanvas],
93
+ ...options.download && manifest.rendering.length > 0 ? { rendering: renderingItems(manifest.rendering) } : {}
94
+ };
95
+ };
96
+
97
+ // src/components/mango-av-player.ts
98
+ var rampModule;
99
+ var loadRamp = () => {
100
+ rampModule ??= import("./ramp-2XKALEGA.js");
101
+ return rampModule;
102
+ };
103
+ var styles = `${componentTheme}
104
+ :host { --mango-av-player-aspect-ratio: 16 / 9; }
105
+ .player { container-type: inline-size; display: flex; flex-direction: column; gap: 12px; }
106
+ .stage { background: #000; border-radius: var(--mango-av-radius); overflow: hidden; width: 100%; }
107
+ .player[data-kind='audio'] .stage { background: var(--mango-av-surface); }
108
+ /*
109
+ * The stage slot holds content shown with the media, such as captions: in the
110
+ * audio card above the controls, or over the foot of a video, clear of its
111
+ * controls and letting clicks through to the player.
112
+ */
113
+ .player[data-kind='video'] .stage { position: relative; }
114
+ .player[data-kind='video'] .stage-slot { bottom: 44px; left: 0; pointer-events: none; position: absolute; right: 0; z-index: 2; }
115
+ .player[data-kind='video'] slot[name=stage]::slotted(*) {
116
+ --mango-av-captions-background: rgb(0 0 0 / .72);
117
+ --mango-av-captions-color: #fff;
118
+ --mango-av-captions-padding: 0 16px;
119
+ }
120
+ .cover { align-items: center; display: flex; gap: 16px; padding: 16px; }
121
+ .cover img { border-radius: 8px; height: 96px; object-fit: cover; width: 96px; }
122
+ .cover h2 { font-family: Georgia, 'Times New Roman', serif; font-size: 22px; font-weight: 400; margin: 4px 0 0; }
123
+ .eyebrow { color: var(--mango-av-muted); font-size: 12px; font-weight: 600; letter-spacing: .04em; text-transform: uppercase; }
124
+ .status { color: var(--mango-av-muted); font-size: 14px; margin: 0; padding: 16px; }
125
+ .error { color: var(--mango-av-danger); font-size: 14px; margin: 0; padding: 16px; }
126
+ .toolbar { align-items: center; display: flex; flex-wrap: wrap; gap: 8px; justify-content: space-between; }
127
+ .group { align-items: center; display: flex; flex-wrap: wrap; gap: 8px; font-size: 14px; }
128
+ .toolbar button, .toolbar select {
129
+ background: var(--mango-av-surface);
130
+ border: 1px solid var(--mango-av-border);
131
+ border-radius: 6px;
132
+ color: inherit;
133
+ height: 34px;
134
+ padding: 0 10px;
135
+ }
136
+ .toolbar button:disabled { cursor: default; opacity: .5; }
137
+ .sr-only { clip: rect(0,0,0,0); clip-path: inset(50%); height: 1px; overflow: hidden; position: absolute; white-space: nowrap; width: 1px; }
138
+
139
+ /*
140
+ * The player wears Video.js's default skin: ramp.ts takes off RAMP's
141
+ * vjs-theme-ramp once the player is ready. Until then, and on any element
142
+ * that keeps the class, don't let the theme's 490 x 270px minimum push the
143
+ * player past a narrow column.
144
+ */
145
+ .ramp .vjs-theme-ramp { min-height: 0; min-width: 0; }
146
+ /* Video.js keeps the bar hidden until playback starts; show it from the start, as before. */
147
+ .ramp .video-js:not(.vjs-has-started) .vjs-control-bar { display: flex; }
148
+ /* RAMP gives audio a 1:0 shape, so the player is the bar alone, in the flow rather than over a picture. */
149
+ .ramp .video-js.vjs-audio-only-mode { height: auto; padding-top: 0; }
150
+ .ramp .vjs-audio-only-mode .vjs-control-bar { position: relative; }
151
+ /* The audio bar is too short to hold its menus, so they open over the card or the page; round its corners instead of clipping. */
152
+ .player[data-kind='audio'] .stage { overflow: visible; }
153
+ .player[data-kind='audio'] .ramp .vjs-control-bar { border-radius: 0 0 var(--mango-av-radius) var(--mango-av-radius); }
154
+ /* RAMP narrows the bar inline, by 24px, to fit its theme's inset. The default skin runs it edge to edge. */
155
+ .ramp .vjs-control-bar { width: 100% !important; }
156
+ /*
157
+ * RAMP's progress bar, and the slider in it, float above the controls even
158
+ * without its theme. Seat them in the bar, growing like Video.js's own.
159
+ */
160
+ .ramp .vjs-control-bar > .vjs-custom-progress-bar { align-self: stretch; flex: auto; height: auto; margin: 0; position: relative; top: auto; width: auto !important; }
161
+ .ramp .vjs-progress-holder.vjs-custom-progress-bar { flex: auto; height: .3em; margin: 0 10px; position: relative; top: auto; width: auto !important; }
162
+ /*
163
+ * Video.js sizes a fluid player with a rule it adds to the document head,
164
+ * which doesn't reach into this shadow root. Give it the video's shape.
165
+ */
166
+ .player[data-kind='video'] .ramp .video-js.vjs-fluid { aspect-ratio: var(--mango-av-player-aspect-ratio); height: auto; padding-top: 0; }
167
+ .player[data-hide~='volume'] .ramp :is(.vjs-volume-panel, .vjs-mute-control) { display: none; }
168
+ .player[data-hide~='seek'] .ramp :is(.vjs-progress-control, .vjs-custom-progress-bar) { display: none; }
169
+ .player[data-hide~='fullscreen'] .ramp .vjs-fullscreen-control { display: none; }
170
+ /* Narrow: drop the controls least needed there, so the rest fit on one row. */
171
+ @container (max-width: 480px) {
172
+ .ramp :is(.vjs-picture-in-picture-control, .vjs-volume-panel) { display: none; }
173
+ }
174
+ @container (max-width: 360px) {
175
+ .ramp :is(.vjs-skip-backward-10, .vjs-skip-backward-5, .vjs-skip-backward-30, .vjs-skip-forward-10, .vjs-skip-forward-5, .vjs-skip-forward-30, .vjs-playback-rate) { display: none; }
176
+ }
177
+ `;
178
+ var MangoAVPlayerElement = class extends AVControllerElement {
179
+ static observedAttributes = ["manifest-url", "config"];
180
+ /**
181
+ * Whether controller events are re-dispatched from this element. The complete
182
+ * element turns this off for the player inside it, which forwards them itself.
183
+ * @internal
184
+ */
185
+ forwardEvents = true;
186
+ #root;
187
+ #manifestInput;
188
+ #config = {};
189
+ #ramp;
190
+ #rampCss = "";
191
+ #media;
192
+ #mountToken = 0;
193
+ constructor() {
194
+ super();
195
+ this.#root = this.attachShadow({ mode: "open" });
196
+ }
197
+ get manifest() {
198
+ return this.#manifestInput;
199
+ }
200
+ set manifest(value) {
201
+ this.#manifestInput = value;
202
+ if (this.isConnected && value !== void 0) void this.load(value);
203
+ }
204
+ get config() {
205
+ return this.#config;
206
+ }
207
+ set config(value) {
208
+ this.#config = value;
209
+ this.controller.configure(value);
210
+ }
211
+ connectedCallback() {
212
+ this.#readConfigAttribute();
213
+ super.connectedCallback();
214
+ const source = this.#manifestInput ?? this.getAttribute("manifest-url") ?? void 0;
215
+ if (source !== void 0 && !this.controller.manifest) void this.load(source);
216
+ }
217
+ disconnectedCallback() {
218
+ this.#unmount();
219
+ super.disconnectedCallback();
220
+ }
221
+ attributeChangedCallback(name, oldValue, newValue) {
222
+ if (!this.isConnected || oldValue === newValue) return;
223
+ if (name === "manifest-url" && newValue) {
224
+ this.#manifestInput = newValue;
225
+ void this.load(newValue);
226
+ } else if (name === "config") {
227
+ this.#readConfigAttribute();
228
+ }
229
+ }
230
+ async load(input) {
231
+ this.#manifestInput = input;
232
+ return await this.controller.load(input);
233
+ }
234
+ play() {
235
+ return this.controller.play();
236
+ }
237
+ pause() {
238
+ this.controller.pause();
239
+ }
240
+ seekTo(time) {
241
+ this.controller.seekTo(time);
242
+ }
243
+ selectCanvas(indexOrId, options) {
244
+ return this.controller.selectCanvas(indexOrId, options);
245
+ }
246
+ controllerChanged() {
247
+ this.#render();
248
+ }
249
+ handleControllerEvent(type, event) {
250
+ if (type === "av-ready" || type === "av-canvaschange" || type === "av-sourcechange" || type === "av-configchange" || type === "av-loadstart") {
251
+ this.#render();
252
+ } else if (type === "av-error" && !this.#ramp) {
253
+ this.#render();
254
+ } else if (type === "av-statechange") {
255
+ this.#syncToolbar();
256
+ }
257
+ if (this.forwardEvents) this.dispatchEvent(new CustomEvent(type, { detail: event.detail, bubbles: true, composed: true }));
258
+ }
259
+ #readConfigAttribute() {
260
+ const value = this.getAttribute("config");
261
+ if (!value) return;
262
+ try {
263
+ this.#config = JSON.parse(value);
264
+ this.controller.configure(this.#config);
265
+ } catch (cause) {
266
+ const error = cause instanceof Error ? cause : new Error(String(cause));
267
+ this.dispatchEvent(new CustomEvent("av-error", { detail: { error }, bubbles: true, composed: true }));
268
+ }
269
+ }
270
+ #render() {
271
+ this.#unmount();
272
+ const state = this.controller.state;
273
+ const canvas = this.controller.canvas;
274
+ const source = this.controller.source;
275
+ const ui = resolveStrings(this.controller.config.strings);
276
+ let body = "";
277
+ if (state.status === "loading") body = `<div class="stage"><p class="status" role="status">${escapeHtml(ui.loadingManifest)}</p></div>`;
278
+ else if (state.status === "error" && !source) body = `<div class="stage"><p class="error" role="alert">${escapeHtml(state.error?.message ?? ui.unableToLoadPlayer)}</p></div>`;
279
+ else if (canvas && source) body = this.#readyHtml(canvas, source);
280
+ else body = `<div class="stage"><p class="status">${escapeHtml(canvas ? ui.noPlayableMedia : ui.noMediaLoaded)}</p></div>`;
281
+ this.#root.innerHTML = `<style data-ramp>${this.#rampCss}</style><style>${styles}</style>
282
+ <div class="player" data-kind="${source?.kind ?? "video"}" data-hide="${this.#hiddenControls()}" style="${this.#aspectStyle(canvas, source)}">${body}</div>`;
283
+ this.#bind();
284
+ const host = this.#root.querySelector(".ramp");
285
+ if (host && canvas && source) void this.#mount(host);
286
+ }
287
+ #readyHtml(canvas, source) {
288
+ const controls = this.controller.config.controls;
289
+ const manifest = this.controller.manifest;
290
+ const ui = resolveStrings(this.controller.config.strings);
291
+ const title = this.controller.config.audioArt.title || manifest?.label || canvas.label;
292
+ const cover = canvas.poster ?? manifest?.thumbnail;
293
+ const showSourceSelector = controls.sourceSelector && canvas.sources.length > 1;
294
+ const showToolbar = controls.navigation || controls.autoAdvance || showSourceSelector;
295
+ return `<div class="stage" aria-label="${escapeHtml(ui.mediaPlayer)}" role="region">
296
+ ${source.kind === "audio" ? `<div class="cover">
297
+ ${cover ? `<img src="${escapeHtml(cover)}" alt="">` : ""}
298
+ <div><span class="eyebrow">${escapeHtml(ui.recording)}</span><h2>${escapeHtml(title)}</h2></div>
299
+ </div>` : ""}
300
+ <div class="stage-slot"><slot name="stage"></slot></div>
301
+ <div class="ramp"></div>
302
+ </div>
303
+ <slot name="after-player"></slot>
304
+ ${showToolbar ? `<div class="toolbar">
305
+ ${controls.navigation ? `<div class="group">
306
+ <button type="button" data-action="previous" aria-label="${escapeHtml(ui.previousSection)}">\u2190 ${escapeHtml(ui.previous)}</button>
307
+ <button type="button" data-action="next" aria-label="${escapeHtml(ui.nextSection)}">${escapeHtml(ui.next)} \u2192</button>
308
+ </div>` : ""}
309
+ ${showSourceSelector || controls.autoAdvance ? `<div class="group">
310
+ ${showSourceSelector ? `<label><span class="sr-only">${escapeHtml(ui.mediaSource)}</span><select data-action="source">${canvas.sources.map((item, index) => `<option value="${index}" ${index === this.controller.state.sourceIndex ? "selected" : ""}>${escapeHtml(item.label ?? item.format ?? formatString(ui.sourceNumber, { number: index + 1 }))}</option>`).join("")}</select></label>` : ""}
311
+ ${controls.autoAdvance ? `<label><input type="checkbox" data-action="auto-advance"> ${escapeHtml(ui.autoAdvance)}</label>` : ""}
312
+ </div>` : ""}
313
+ </div>` : ""}`;
314
+ }
315
+ #hiddenControls() {
316
+ const controls = this.controller.config.controls;
317
+ return [
318
+ controls.volume ? "" : "volume",
319
+ controls.seek ? "" : "seek",
320
+ controls.fullscreen ? "" : "fullscreen"
321
+ ].filter(Boolean).join(" ");
322
+ }
323
+ #aspectStyle(canvas, source) {
324
+ const width = source?.width ?? canvas?.width;
325
+ const height = source?.height ?? canvas?.height;
326
+ return width && height ? `--mango-av-player-aspect-ratio: ${width} / ${height}` : "";
327
+ }
328
+ #bind() {
329
+ this.#root.querySelector("[data-action=previous]")?.addEventListener("click", () => this.controller.previous({ autoplay: this.controller.state.status === "playing" }));
330
+ this.#root.querySelector("[data-action=next]")?.addEventListener("click", () => this.controller.next({ autoplay: this.controller.state.status === "playing" }));
331
+ this.#root.querySelector("[data-action=source]")?.addEventListener("change", (event) => this.controller.selectSource(Number(event.target.value)));
332
+ this.#root.querySelector("[data-action=auto-advance]")?.addEventListener("change", (event) => this.controller.setAutoAdvance(event.target.checked));
333
+ this.#syncToolbar();
334
+ }
335
+ #syncToolbar() {
336
+ const canvas = this.controller.canvas;
337
+ const count = this.controller.manifest?.canvases.length ?? 0;
338
+ const state = this.controller.state;
339
+ const previous = this.#root.querySelector("[data-action=previous]");
340
+ const next = this.#root.querySelector("[data-action=next]");
341
+ const autoAdvance = this.#root.querySelector("[data-action=auto-advance]");
342
+ if (previous) previous.disabled = !canvas || canvas.index === 0;
343
+ if (next) next.disabled = !canvas || canvas.index >= count - 1 && !state.repeat;
344
+ if (autoAdvance) autoAdvance.checked = state.autoAdvance;
345
+ }
346
+ async #mount(host) {
347
+ const token = ++this.#mountToken;
348
+ const controller = this.controller;
349
+ const manifest = controller.manifest;
350
+ const canvas = controller.canvas;
351
+ const source = controller.source;
352
+ if (!manifest || !canvas || !source) return;
353
+ try {
354
+ const [src, ramp] = await Promise.all([controller.resolvedSourceUrl(), loadRamp()]);
355
+ if (token !== this.#mountToken || !host.isConnected || !src) return;
356
+ if (!this.#rampCss) {
357
+ this.#rampCss = ramp.rampCss;
358
+ const style = this.#root.querySelector("style[data-ramp]");
359
+ if (style) style.textContent = ramp.rampCss;
360
+ }
361
+ const config = controller.config;
362
+ const { controls } = config;
363
+ const options = {
364
+ manifest: rampManifest(manifest, canvas, source, { src, captions: Boolean(controls.captions), download: Boolean(controls.download) }),
365
+ language: config.preferredLanguages.find((value) => value !== "none") ?? "en",
366
+ playbackRate: Boolean(controls.playbackRate),
367
+ playbackRates: config.playbackRates,
368
+ pictureInPicture: Boolean(controls.pictureInPicture) && source.kind === "video",
369
+ download: Boolean(controls.download),
370
+ withCredentials: config.withCredentials,
371
+ resume: { enable: Boolean(config.resume.enabled), ttlDays: config.resume.ttlDays ?? 30, maxItems: config.resume.maxItems ?? 200 },
372
+ onMedia: (media) => {
373
+ if (token !== this.#mountToken) return;
374
+ this.#media = media;
375
+ controller.attachMedia(media);
376
+ }
377
+ };
378
+ if (controls.seek && controls.skipSeconds) options.skipSeconds = controls.skipSeconds;
379
+ this.#ramp = ramp.mountRamp(host, options);
380
+ } catch (cause) {
381
+ if (token !== this.#mountToken) return;
382
+ const error = cause instanceof Error ? cause : new Error(String(cause));
383
+ this.dispatchEvent(new CustomEvent("av-error", { detail: { error }, bubbles: true, composed: true }));
384
+ }
385
+ }
386
+ #unmount() {
387
+ this.#mountToken += 1;
388
+ if (this.#media && this.controller.media === this.#media) this.controller.detachMedia();
389
+ this.#media = void 0;
390
+ this.#ramp?.destroy();
391
+ this.#ramp = void 0;
392
+ }
393
+ };
394
+ var defineMangoAVPlayer = (tagName = "mango-av-player") => {
395
+ if (typeof customElements === "undefined") return MangoAVPlayerElement;
396
+ if (!MangoAVPlayerElement.registeredTag && !customElements.get(tagName)) {
397
+ customElements.define(tagName, MangoAVPlayerElement);
398
+ MangoAVPlayerElement.registeredTag = tagName;
399
+ }
400
+ return MangoAVPlayerElement;
401
+ };
402
+
403
+ export {
404
+ MangoAVPlayerElement,
405
+ defineMangoAVPlayer
406
+ };
407
+ //# sourceMappingURL=chunk-BB7RE7EO.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/components/ramp-manifest.ts","../src/components/mango-av-player.ts"],"sourcesContent":["import type { AVCanvas, AVManifest, DownloadResource, JsonObject, MediaSource } from '../types';\n\nexport interface RampManifestOptions {\n /** The URL to play, after `resolveRequest`. */\n src: string;\n captions: boolean;\n download: boolean;\n}\n\nconst text = (value: string): JsonObject => ({ none: [value] });\n\nconst renderingItems = (files: DownloadResource[]): JsonObject[] =>\n files.map((file) => ({\n id: file.id,\n type: 'Text',\n label: text(file.label),\n ...(file.format ? { format: file.format } : {}),\n }));\n\n/*\n * RAMP reads Presentation 3 only, and plays every Canvas in the Manifest it is\n * given. It gets a Manifest holding just the active Canvas and source, rebuilt\n * from the normalized model, so Presentation 2 Manifests, `resolveRequest` and\n * the source selector work, and its own navigation can't drift from the\n * controller's. Transcripts, chapters and annotations stay with this package.\n */\nexport const rampManifest = (\n manifest: AVManifest,\n canvas: AVCanvas,\n source: MediaSource,\n options: RampManifestOptions,\n): JsonObject => {\n const duration = canvas.duration ?? source.duration;\n // RAMP shows these through Video.js, which only knows caption and subtitle files.\n const tracks = options.captions\n ? canvas.tracks.filter((track) => track.kind === 'captions' || track.kind === 'subtitles')\n : [];\n const body: JsonObject = {\n id: options.src,\n type: source.kind === 'audio' ? 'Sound' : 'Video',\n ...(source.format ? { format: source.format } : {}),\n ...(source.width ? { width: source.width } : {}),\n ...(source.height ? { height: source.height } : {}),\n ...(duration ? { duration } : {}),\n };\n const rampCanvas: JsonObject = {\n id: canvas.id,\n type: 'Canvas',\n label: text(canvas.label),\n ...(duration ? { duration } : {}),\n ...(canvas.width ? { width: canvas.width } : {}),\n ...(canvas.height ? { height: canvas.height } : {}),\n items: [{\n id: `${canvas.id}/ramp/page`,\n type: 'AnnotationPage',\n items: [{\n id: `${canvas.id}/ramp/painting`,\n type: 'Annotation',\n motivation: 'painting',\n body,\n target: canvas.id,\n }],\n }],\n };\n if (tracks.length > 0) {\n rampCanvas.annotations = [{\n id: `${canvas.id}/ramp/captions`,\n type: 'AnnotationPage',\n items: tracks.map((track, index) => ({\n id: `${canvas.id}/ramp/captions/${index}`,\n type: 'Annotation',\n motivation: 'supplementing',\n body: {\n id: track.src,\n type: 'Text',\n // RAMP reads the format to choose the track kind, and fails without one.\n format: track.format ?? 'text/vtt',\n label: text(track.label),\n ...(track.language ? { language: track.language } : {}),\n },\n target: canvas.id,\n })),\n }];\n }\n if (canvas.poster && source.kind === 'video') {\n rampCanvas.placeholderCanvas = {\n id: `${canvas.id}/ramp/poster`,\n type: 'Canvas',\n items: [{\n id: `${canvas.id}/ramp/poster/page`,\n type: 'AnnotationPage',\n items: [{\n id: `${canvas.id}/ramp/poster/painting`,\n type: 'Annotation',\n motivation: 'painting',\n body: { id: canvas.poster, type: 'Image' },\n target: `${canvas.id}/ramp/poster`,\n }],\n }],\n };\n }\n if (options.download && canvas.rendering.length > 0) rampCanvas.rendering = renderingItems(canvas.rendering);\n return {\n '@context': 'http://iiif.io/api/presentation/3/context.json',\n id: manifest.id,\n type: 'Manifest',\n label: text(manifest.label),\n items: [rampCanvas],\n ...(options.download && manifest.rendering.length > 0 ? { rendering: renderingItems(manifest.rendering) } : {}),\n };\n};\n","import { AVControllerElement } from './controller-element';\nimport { componentTheme } from './component-styles';\nimport { rampManifest } from './ramp-manifest';\nimport type { RampHandle } from './ramp';\nimport type { AVCanvas, AVManifest, AVPlayerConfig, AVPlayerEventMap, JsonObject, MediaSource } from '../types';\nimport { escapeHtml, formatString, resolveStrings } from '../utils';\n\ntype RampModule = typeof import('./ramp');\nlet rampModule: Promise<RampModule> | undefined;\nconst loadRamp = (): Promise<RampModule> => {\n rampModule ??= import('./ramp');\n return rampModule;\n};\n\nconst styles = `${componentTheme}\n :host { --mango-av-player-aspect-ratio: 16 / 9; }\n .player { container-type: inline-size; display: flex; flex-direction: column; gap: 12px; }\n .stage { background: #000; border-radius: var(--mango-av-radius); overflow: hidden; width: 100%; }\n .player[data-kind='audio'] .stage { background: var(--mango-av-surface); }\n /*\n * The stage slot holds content shown with the media, such as captions: in the\n * audio card above the controls, or over the foot of a video, clear of its\n * controls and letting clicks through to the player.\n */\n .player[data-kind='video'] .stage { position: relative; }\n .player[data-kind='video'] .stage-slot { bottom: 44px; left: 0; pointer-events: none; position: absolute; right: 0; z-index: 2; }\n .player[data-kind='video'] slot[name=stage]::slotted(*) {\n --mango-av-captions-background: rgb(0 0 0 / .72);\n --mango-av-captions-color: #fff;\n --mango-av-captions-padding: 0 16px;\n }\n .cover { align-items: center; display: flex; gap: 16px; padding: 16px; }\n .cover img { border-radius: 8px; height: 96px; object-fit: cover; width: 96px; }\n .cover h2 { font-family: Georgia, 'Times New Roman', serif; font-size: 22px; font-weight: 400; margin: 4px 0 0; }\n .eyebrow { color: var(--mango-av-muted); font-size: 12px; font-weight: 600; letter-spacing: .04em; text-transform: uppercase; }\n .status { color: var(--mango-av-muted); font-size: 14px; margin: 0; padding: 16px; }\n .error { color: var(--mango-av-danger); font-size: 14px; margin: 0; padding: 16px; }\n .toolbar { align-items: center; display: flex; flex-wrap: wrap; gap: 8px; justify-content: space-between; }\n .group { align-items: center; display: flex; flex-wrap: wrap; gap: 8px; font-size: 14px; }\n .toolbar button, .toolbar select {\n background: var(--mango-av-surface);\n border: 1px solid var(--mango-av-border);\n border-radius: 6px;\n color: inherit;\n height: 34px;\n padding: 0 10px;\n }\n .toolbar button:disabled { cursor: default; opacity: .5; }\n .sr-only { clip: rect(0,0,0,0); clip-path: inset(50%); height: 1px; overflow: hidden; position: absolute; white-space: nowrap; width: 1px; }\n\n /*\n * The player wears Video.js's default skin: ramp.ts takes off RAMP's\n * vjs-theme-ramp once the player is ready. Until then, and on any element\n * that keeps the class, don't let the theme's 490 x 270px minimum push the\n * player past a narrow column.\n */\n .ramp .vjs-theme-ramp { min-height: 0; min-width: 0; }\n /* Video.js keeps the bar hidden until playback starts; show it from the start, as before. */\n .ramp .video-js:not(.vjs-has-started) .vjs-control-bar { display: flex; }\n /* RAMP gives audio a 1:0 shape, so the player is the bar alone, in the flow rather than over a picture. */\n .ramp .video-js.vjs-audio-only-mode { height: auto; padding-top: 0; }\n .ramp .vjs-audio-only-mode .vjs-control-bar { position: relative; }\n /* The audio bar is too short to hold its menus, so they open over the card or the page; round its corners instead of clipping. */\n .player[data-kind='audio'] .stage { overflow: visible; }\n .player[data-kind='audio'] .ramp .vjs-control-bar { border-radius: 0 0 var(--mango-av-radius) var(--mango-av-radius); }\n /* RAMP narrows the bar inline, by 24px, to fit its theme's inset. The default skin runs it edge to edge. */\n .ramp .vjs-control-bar { width: 100% !important; }\n /*\n * RAMP's progress bar, and the slider in it, float above the controls even\n * without its theme. Seat them in the bar, growing like Video.js's own.\n */\n .ramp .vjs-control-bar > .vjs-custom-progress-bar { align-self: stretch; flex: auto; height: auto; margin: 0; position: relative; top: auto; width: auto !important; }\n .ramp .vjs-progress-holder.vjs-custom-progress-bar { flex: auto; height: .3em; margin: 0 10px; position: relative; top: auto; width: auto !important; }\n /*\n * Video.js sizes a fluid player with a rule it adds to the document head,\n * which doesn't reach into this shadow root. Give it the video's shape.\n */\n .player[data-kind='video'] .ramp .video-js.vjs-fluid { aspect-ratio: var(--mango-av-player-aspect-ratio); height: auto; padding-top: 0; }\n .player[data-hide~='volume'] .ramp :is(.vjs-volume-panel, .vjs-mute-control) { display: none; }\n .player[data-hide~='seek'] .ramp :is(.vjs-progress-control, .vjs-custom-progress-bar) { display: none; }\n .player[data-hide~='fullscreen'] .ramp .vjs-fullscreen-control { display: none; }\n /* Narrow: drop the controls least needed there, so the rest fit on one row. */\n @container (max-width: 480px) {\n .ramp :is(.vjs-picture-in-picture-control, .vjs-volume-panel) { display: none; }\n }\n @container (max-width: 360px) {\n .ramp :is(.vjs-skip-backward-10, .vjs-skip-backward-5, .vjs-skip-backward-30, .vjs-skip-forward-10, .vjs-skip-forward-5, .vjs-skip-forward-30, .vjs-playback-rate) { display: none; }\n }\n`;\n\nexport class MangoAVPlayerElement extends AVControllerElement {\n static observedAttributes = ['manifest-url', 'config'];\n\n /**\n * Whether controller events are re-dispatched from this element. The complete\n * element turns this off for the player inside it, which forwards them itself.\n * @internal\n */\n forwardEvents = true;\n\n #root: ShadowRoot;\n #manifestInput: JsonObject | string | undefined;\n #config: AVPlayerConfig = {};\n #ramp: RampHandle | undefined;\n #rampCss = '';\n #media: HTMLMediaElement | undefined;\n #mountToken = 0;\n\n constructor() {\n super();\n this.#root = this.attachShadow({ mode: 'open' });\n }\n\n get manifest(): JsonObject | string | undefined {\n return this.#manifestInput;\n }\n\n set manifest(value: JsonObject | string | undefined) {\n this.#manifestInput = value;\n if (this.isConnected && value !== undefined) void this.load(value);\n }\n\n get config(): AVPlayerConfig {\n return this.#config;\n }\n\n set config(value: AVPlayerConfig) {\n this.#config = value;\n this.controller.configure(value);\n }\n\n override connectedCallback(): void {\n this.#readConfigAttribute();\n super.connectedCallback();\n const source = this.#manifestInput ?? this.getAttribute('manifest-url') ?? undefined;\n if (source !== undefined && !this.controller.manifest) void this.load(source);\n }\n\n override disconnectedCallback(): void {\n this.#unmount();\n super.disconnectedCallback();\n }\n\n attributeChangedCallback(name: string, oldValue: string | null, newValue: string | null): void {\n if (!this.isConnected || oldValue === newValue) return;\n if (name === 'manifest-url' && newValue) {\n this.#manifestInput = newValue;\n void this.load(newValue);\n } else if (name === 'config') {\n this.#readConfigAttribute();\n }\n }\n\n async load(input: JsonObject | string): Promise<AVManifest> {\n this.#manifestInput = input;\n return await this.controller.load(input);\n }\n\n play(): Promise<void> { return this.controller.play(); }\n pause(): void { this.controller.pause(); }\n seekTo(time: number): void { this.controller.seekTo(time); }\n selectCanvas(indexOrId: number | string, options?: { time?: number; autoplay?: boolean }): AVCanvas | undefined {\n return this.controller.selectCanvas(indexOrId, options);\n }\n\n protected controllerChanged(): void {\n this.#render();\n }\n\n protected handleControllerEvent<K extends keyof AVPlayerEventMap>(type: K, event: CustomEvent<AVPlayerEventMap[K]>): void {\n if (type === 'av-ready' || type === 'av-canvaschange' || type === 'av-sourcechange' || type === 'av-configchange' || type === 'av-loadstart') {\n this.#render();\n } else if (type === 'av-error' && !this.#ramp) {\n // RAMP reports media errors over its own player; only a failed load is shown here.\n this.#render();\n } else if (type === 'av-statechange') {\n this.#syncToolbar();\n }\n if (this.forwardEvents) this.dispatchEvent(new CustomEvent(type, { detail: event.detail, bubbles: true, composed: true }));\n }\n\n #readConfigAttribute(): void {\n const value = this.getAttribute('config');\n if (!value) return;\n try {\n this.#config = JSON.parse(value) as AVPlayerConfig;\n this.controller.configure(this.#config);\n } catch (cause) {\n const error = cause instanceof Error ? cause : new Error(String(cause));\n this.dispatchEvent(new CustomEvent('av-error', { detail: { error }, bubbles: true, composed: true }));\n }\n }\n\n #render(): void {\n this.#unmount();\n const state = this.controller.state;\n const canvas = this.controller.canvas;\n const source = this.controller.source;\n const ui = resolveStrings(this.controller.config.strings);\n let body = '';\n if (state.status === 'loading') body = `<div class=\"stage\"><p class=\"status\" role=\"status\">${escapeHtml(ui.loadingManifest)}</p></div>`;\n else if (state.status === 'error' && !source) body = `<div class=\"stage\"><p class=\"error\" role=\"alert\">${escapeHtml(state.error?.message ?? ui.unableToLoadPlayer)}</p></div>`;\n else if (canvas && source) body = this.#readyHtml(canvas, source);\n else body = `<div class=\"stage\"><p class=\"status\">${escapeHtml(canvas ? ui.noPlayableMedia : ui.noMediaLoaded)}</p></div>`;\n // RAMP's stylesheet goes first, so the adjustments here win its ties.\n this.#root.innerHTML = `<style data-ramp>${this.#rampCss}</style><style>${styles}</style>\n <div class=\"player\" data-kind=\"${source?.kind ?? 'video'}\" data-hide=\"${this.#hiddenControls()}\" style=\"${this.#aspectStyle(canvas, source)}\">${body}</div>`;\n this.#bind();\n const host = this.#root.querySelector<HTMLElement>('.ramp');\n if (host && canvas && source) void this.#mount(host);\n }\n\n #readyHtml(canvas: AVCanvas, source: MediaSource): string {\n const controls = this.controller.config.controls;\n const manifest = this.controller.manifest;\n const ui = resolveStrings(this.controller.config.strings);\n const title = this.controller.config.audioArt.title || manifest?.label || canvas.label;\n const cover = canvas.poster ?? manifest?.thumbnail;\n const showSourceSelector = controls.sourceSelector && canvas.sources.length > 1;\n const showToolbar = controls.navigation || controls.autoAdvance || showSourceSelector;\n return `<div class=\"stage\" aria-label=\"${escapeHtml(ui.mediaPlayer)}\" role=\"region\">\n ${source.kind === 'audio' ? `<div class=\"cover\">\n ${cover ? `<img src=\"${escapeHtml(cover)}\" alt=\"\">` : ''}\n <div><span class=\"eyebrow\">${escapeHtml(ui.recording)}</span><h2>${escapeHtml(title)}</h2></div>\n </div>` : ''}\n <div class=\"stage-slot\"><slot name=\"stage\"></slot></div>\n <div class=\"ramp\"></div>\n </div>\n <slot name=\"after-player\"></slot>\n ${showToolbar ? `<div class=\"toolbar\">\n ${controls.navigation ? `<div class=\"group\">\n <button type=\"button\" data-action=\"previous\" aria-label=\"${escapeHtml(ui.previousSection)}\">← ${escapeHtml(ui.previous)}</button>\n <button type=\"button\" data-action=\"next\" aria-label=\"${escapeHtml(ui.nextSection)}\">${escapeHtml(ui.next)} →</button>\n </div>` : ''}\n ${showSourceSelector || controls.autoAdvance ? `<div class=\"group\">\n ${showSourceSelector ? `<label><span class=\"sr-only\">${escapeHtml(ui.mediaSource)}</span><select data-action=\"source\">${canvas.sources.map((item, index) => `<option value=\"${index}\" ${index === this.controller.state.sourceIndex ? 'selected' : ''}>${escapeHtml(item.label ?? item.format ?? formatString(ui.sourceNumber, { number: index + 1 }))}</option>`).join('')}</select></label>` : ''}\n ${controls.autoAdvance ? `<label><input type=\"checkbox\" data-action=\"auto-advance\"> ${escapeHtml(ui.autoAdvance)}</label>` : ''}\n </div>` : ''}\n </div>` : ''}`;\n }\n\n #hiddenControls(): string {\n const controls = this.controller.config.controls;\n return [\n controls.volume ? '' : 'volume',\n controls.seek ? '' : 'seek',\n controls.fullscreen ? '' : 'fullscreen',\n ].filter(Boolean).join(' ');\n }\n\n #aspectStyle(canvas: AVCanvas | undefined, source: MediaSource | undefined): string {\n const width = source?.width ?? canvas?.width;\n const height = source?.height ?? canvas?.height;\n return width && height ? `--mango-av-player-aspect-ratio: ${width} / ${height}` : '';\n }\n\n #bind(): void {\n this.#root.querySelector<HTMLElement>('[data-action=previous]')?.addEventListener('click', () => this.controller.previous({ autoplay: this.controller.state.status === 'playing' }));\n this.#root.querySelector<HTMLElement>('[data-action=next]')?.addEventListener('click', () => this.controller.next({ autoplay: this.controller.state.status === 'playing' }));\n this.#root.querySelector<HTMLSelectElement>('[data-action=source]')?.addEventListener('change', (event) => this.controller.selectSource(Number((event.target as HTMLSelectElement).value)));\n this.#root.querySelector<HTMLInputElement>('[data-action=auto-advance]')?.addEventListener('change', (event) => this.controller.setAutoAdvance((event.target as HTMLInputElement).checked));\n this.#syncToolbar();\n }\n\n #syncToolbar(): void {\n const canvas = this.controller.canvas;\n const count = this.controller.manifest?.canvases.length ?? 0;\n const state = this.controller.state;\n const previous = this.#root.querySelector<HTMLButtonElement>('[data-action=previous]');\n const next = this.#root.querySelector<HTMLButtonElement>('[data-action=next]');\n const autoAdvance = this.#root.querySelector<HTMLInputElement>('[data-action=auto-advance]');\n if (previous) previous.disabled = !canvas || canvas.index === 0;\n if (next) next.disabled = !canvas || (canvas.index >= count - 1 && !state.repeat);\n if (autoAdvance) autoAdvance.checked = state.autoAdvance;\n }\n\n async #mount(host: HTMLElement): Promise<void> {\n const token = ++this.#mountToken;\n const controller = this.controller;\n const manifest = controller.manifest;\n const canvas = controller.canvas;\n const source = controller.source;\n if (!manifest || !canvas || !source) return;\n try {\n const [src, ramp] = await Promise.all([controller.resolvedSourceUrl(), loadRamp()]);\n if (token !== this.#mountToken || !host.isConnected || !src) return;\n if (!this.#rampCss) {\n this.#rampCss = ramp.rampCss;\n const style = this.#root.querySelector('style[data-ramp]');\n if (style) style.textContent = ramp.rampCss;\n }\n const config = controller.config;\n const { controls } = config;\n const options: Parameters<RampModule['mountRamp']>[1] = {\n manifest: rampManifest(manifest, canvas, source, { src, captions: Boolean(controls.captions), download: Boolean(controls.download) }),\n language: config.preferredLanguages.find((value) => value !== 'none') ?? 'en',\n playbackRate: Boolean(controls.playbackRate),\n playbackRates: config.playbackRates,\n pictureInPicture: Boolean(controls.pictureInPicture) && source.kind === 'video',\n download: Boolean(controls.download),\n withCredentials: config.withCredentials,\n resume: { enable: Boolean(config.resume.enabled), ttlDays: config.resume.ttlDays ?? 30, maxItems: config.resume.maxItems ?? 200 },\n onMedia: (media) => {\n if (token !== this.#mountToken) return;\n this.#media = media;\n controller.attachMedia(media);\n },\n };\n if (controls.seek && controls.skipSeconds) options.skipSeconds = controls.skipSeconds;\n this.#ramp = ramp.mountRamp(host, options);\n } catch (cause) {\n if (token !== this.#mountToken) return;\n const error = cause instanceof Error ? cause : new Error(String(cause));\n this.dispatchEvent(new CustomEvent('av-error', { detail: { error }, bubbles: true, composed: true }));\n }\n }\n\n #unmount(): void {\n this.#mountToken += 1;\n if (this.#media && this.controller.media === this.#media) this.controller.detachMedia();\n this.#media = undefined;\n this.#ramp?.destroy();\n this.#ramp = undefined;\n }\n}\n\nexport const defineMangoAVPlayer = (tagName = 'mango-av-player'): typeof MangoAVPlayerElement => {\n if (typeof customElements === 'undefined') return MangoAVPlayerElement;\n if (!MangoAVPlayerElement.registeredTag && !customElements.get(tagName)) {\n customElements.define(tagName, MangoAVPlayerElement);\n MangoAVPlayerElement.registeredTag = tagName;\n }\n return MangoAVPlayerElement;\n};\n"],"mappings":"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","names":[]}
@@ -0,0 +1,86 @@
1
+ import {
2
+ AVControllerElement,
3
+ componentTheme,
4
+ panelStyles
5
+ } from "./chunk-JXZR5DL2.js";
6
+ import {
7
+ escapeHtml,
8
+ formatTime,
9
+ resolveStrings
10
+ } from "./chunk-53ZN4Y4L.js";
11
+
12
+ // src/components/mango-av-chapters.ts
13
+ var styles = `${componentTheme}${panelStyles}`;
14
+ var renderChapters = (chapters, ui, level = 1) => {
15
+ if (chapters.length === 0) return level === 1 ? `<p class="empty">${escapeHtml(ui.noChapters)}</p>` : "";
16
+ return `<ul class="list">${chapters.map((chapter) => `<li ${chapter.canvasId ? `data-canvas="${escapeHtml(chapter.canvasId)}"` : ""} ${chapter.time ? `data-time="${chapter.time.start}" data-end="${chapter.time.end ?? ""}"` : ""}>
17
+ <button type="button" class="row" ${chapter.canvasId ? "" : "disabled"}>
18
+ <time>${chapter.time ? formatTime(chapter.time.start) : ""}</time>
19
+ <span>${escapeHtml(chapter.label)}</span>
20
+ </button>
21
+ ${renderChapters(chapter.children, ui, level + 1)}
22
+ </li>`).join("")}</ul>`;
23
+ };
24
+ var MangoAVChaptersElement = class extends AVControllerElement {
25
+ #root;
26
+ constructor() {
27
+ super();
28
+ this.#root = this.attachShadow({ mode: "open" });
29
+ }
30
+ controllerChanged() {
31
+ this.#render();
32
+ }
33
+ handleControllerEvent(type, _event) {
34
+ if (type === "av-ready" || type === "av-canvaschange" || type === "av-configchange") this.#render();
35
+ else if (type === "av-timeupdate" || type === "av-seek") this.#sync();
36
+ }
37
+ #render() {
38
+ const manifest = this.controller.manifest;
39
+ const ui = resolveStrings(this.controller.config.strings);
40
+ this.#root.innerHTML = `<style>${styles}</style><section class="panel" aria-label="${escapeHtml(ui.chapters)}">
41
+ <slot name="heading"><h2>${escapeHtml(ui.chapters)}</h2></slot>
42
+ ${manifest ? renderChapters(manifest.chapters, ui) : `<p class="empty">${escapeHtml(ui.noManifest)}</p>`}
43
+ </section>`;
44
+ this.#root.querySelectorAll("[data-canvas] > button").forEach((button) => button.addEventListener("click", () => {
45
+ const canvasId = button.parentElement?.dataset.canvas;
46
+ if (!canvasId) return;
47
+ const time = Number(button.parentElement?.dataset.time ?? 0);
48
+ this.dispatchEvent(new CustomEvent("av-chapterselect", {
49
+ detail: { canvasId, time },
50
+ bubbles: true,
51
+ composed: true
52
+ }));
53
+ this.controller.selectCanvas(canvasId, { time, autoplay: this.controller.state.status === "playing" });
54
+ }));
55
+ this.#sync();
56
+ }
57
+ /* A chapter is current while its Canvas plays and the time is within it; one without a time covers its Canvas. */
58
+ #sync() {
59
+ const canvasId = this.controller.canvas?.id;
60
+ const time = this.controller.state.currentTime;
61
+ this.#root.querySelectorAll("[data-canvas]").forEach((row) => {
62
+ const start = Number(row.dataset.time);
63
+ const end = row.dataset.end ? Number(row.dataset.end) : Number.NaN;
64
+ const inTime = row.dataset.time === void 0 || time >= start && (!Number.isFinite(end) || time < end);
65
+ const active = row.dataset.canvas === canvasId && inTime;
66
+ const button = row.querySelector(":scope > button");
67
+ button?.classList.toggle("active", active);
68
+ if (active) button?.setAttribute("aria-current", "true");
69
+ else button?.removeAttribute("aria-current");
70
+ });
71
+ }
72
+ };
73
+ var defineMangoAVChapters = (tagName = "mango-av-chapters") => {
74
+ if (typeof customElements === "undefined") return MangoAVChaptersElement;
75
+ if (!MangoAVChaptersElement.registeredTag && !customElements.get(tagName)) {
76
+ customElements.define(tagName, MangoAVChaptersElement);
77
+ MangoAVChaptersElement.registeredTag = tagName;
78
+ }
79
+ return MangoAVChaptersElement;
80
+ };
81
+
82
+ export {
83
+ MangoAVChaptersElement,
84
+ defineMangoAVChapters
85
+ };
86
+ //# sourceMappingURL=chunk-BMMQFCD3.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/components/mango-av-chapters.ts"],"sourcesContent":["import { AVControllerElement } from './controller-element';\nimport { componentTheme, panelStyles } from './component-styles';\nimport type { AVPlayerEventMap, AVStrings, Chapter } from '../types';\nimport { escapeHtml, formatTime, resolveStrings } from '../utils';\n\nconst styles = `${componentTheme}${panelStyles}`;\n\nconst renderChapters = (chapters: Chapter[], ui: AVStrings, level = 1): string => {\n if (chapters.length === 0) return level === 1 ? `<p class=\"empty\">${escapeHtml(ui.noChapters)}</p>` : '';\n return `<ul class=\"list\">${chapters.map((chapter) => `<li ${chapter.canvasId ? `data-canvas=\"${escapeHtml(chapter.canvasId)}\"` : ''} ${chapter.time ? `data-time=\"${chapter.time.start}\" data-end=\"${chapter.time.end ?? ''}\"` : ''}>\n <button type=\"button\" class=\"row\" ${chapter.canvasId ? '' : 'disabled'}>\n <time>${chapter.time ? formatTime(chapter.time.start) : ''}</time>\n <span>${escapeHtml(chapter.label)}</span>\n </button>\n ${renderChapters(chapter.children, ui, level + 1)}\n </li>`).join('')}</ul>`;\n};\n\nexport class MangoAVChaptersElement extends AVControllerElement {\n #root: ShadowRoot;\n\n constructor() {\n super();\n this.#root = this.attachShadow({ mode: 'open' });\n }\n\n protected controllerChanged(): void { this.#render(); }\n\n protected handleControllerEvent<K extends keyof AVPlayerEventMap>(type: K, _event: CustomEvent<AVPlayerEventMap[K]>): void {\n if (type === 'av-ready' || type === 'av-canvaschange' || type === 'av-configchange') this.#render();\n else if (type === 'av-timeupdate' || type === 'av-seek') this.#sync();\n }\n\n #render(): void {\n const manifest = this.controller.manifest;\n const ui = resolveStrings(this.controller.config.strings);\n this.#root.innerHTML = `<style>${styles}</style><section class=\"panel\" aria-label=\"${escapeHtml(ui.chapters)}\">\n <slot name=\"heading\"><h2>${escapeHtml(ui.chapters)}</h2></slot>\n ${manifest ? renderChapters(manifest.chapters, ui) : `<p class=\"empty\">${escapeHtml(ui.noManifest)}</p>`}\n </section>`;\n this.#root.querySelectorAll<HTMLElement>('[data-canvas] > button').forEach((button) => button.addEventListener('click', () => {\n const canvasId = button.parentElement?.dataset.canvas;\n if (!canvasId) return;\n const time = Number(button.parentElement?.dataset.time ?? 0);\n this.dispatchEvent(new CustomEvent('av-chapterselect', {\n detail: { canvasId, time },\n bubbles: true,\n composed: true,\n }));\n this.controller.selectCanvas(canvasId, { time, autoplay: this.controller.state.status === 'playing' });\n }));\n this.#sync();\n }\n\n /* A chapter is current while its Canvas plays and the time is within it; one without a time covers its Canvas. */\n #sync(): void {\n const canvasId = this.controller.canvas?.id;\n const time = this.controller.state.currentTime;\n this.#root.querySelectorAll<HTMLElement>('[data-canvas]').forEach((row) => {\n const start = Number(row.dataset.time);\n const end = row.dataset.end ? Number(row.dataset.end) : Number.NaN;\n const inTime = row.dataset.time === undefined || (time >= start && (!Number.isFinite(end) || time < end));\n const active = row.dataset.canvas === canvasId && inTime;\n const button = row.querySelector(':scope > button');\n button?.classList.toggle('active', active);\n if (active) button?.setAttribute('aria-current', 'true');\n else button?.removeAttribute('aria-current');\n });\n }\n}\n\nexport const defineMangoAVChapters = (tagName = 'mango-av-chapters'): typeof MangoAVChaptersElement => {\n if (typeof customElements === 'undefined') return MangoAVChaptersElement;\n if (!MangoAVChaptersElement.registeredTag && !customElements.get(tagName)) {\n customElements.define(tagName, MangoAVChaptersElement);\n MangoAVChaptersElement.registeredTag = tagName;\n }\n return MangoAVChaptersElement;\n};\n"],"mappings":"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","names":[]}