vidply 1.0.27 → 1.0.28

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 (52) hide show
  1. package/dist/dev/vidply.TranscriptManager-QSF2PWUN.js +1744 -0
  2. package/dist/dev/vidply.TranscriptManager-QSF2PWUN.js.map +7 -0
  3. package/dist/dev/vidply.TranscriptManager-UTJBQC5B.js +1744 -0
  4. package/dist/dev/vidply.TranscriptManager-UTJBQC5B.js.map +7 -0
  5. package/dist/dev/vidply.chunk-5663PYKK.js +1631 -0
  6. package/dist/dev/vidply.chunk-5663PYKK.js.map +7 -0
  7. package/dist/dev/vidply.chunk-SRM7VNHG.js +1638 -0
  8. package/dist/dev/vidply.chunk-SRM7VNHG.js.map +7 -0
  9. package/dist/dev/vidply.de-RXAJM5QE.js +181 -0
  10. package/dist/dev/vidply.de-RXAJM5QE.js.map +7 -0
  11. package/dist/dev/vidply.de-SNL6AJ4D.js +188 -0
  12. package/dist/dev/vidply.de-SNL6AJ4D.js.map +7 -0
  13. package/dist/dev/vidply.es-2QCQKZ4U.js +188 -0
  14. package/dist/dev/vidply.es-2QCQKZ4U.js.map +7 -0
  15. package/dist/dev/vidply.es-SADVLJTQ.js +181 -0
  16. package/dist/dev/vidply.es-SADVLJTQ.js.map +7 -0
  17. package/dist/dev/vidply.esm.js +9 -9
  18. package/dist/dev/vidply.esm.js.map +2 -2
  19. package/dist/dev/vidply.fr-FJAZRL4L.js +188 -0
  20. package/dist/dev/vidply.fr-FJAZRL4L.js.map +7 -0
  21. package/dist/dev/vidply.fr-V3VAYBBT.js +181 -0
  22. package/dist/dev/vidply.fr-V3VAYBBT.js.map +7 -0
  23. package/dist/dev/vidply.ja-2XQOW53T.js +188 -0
  24. package/dist/dev/vidply.ja-2XQOW53T.js.map +7 -0
  25. package/dist/dev/vidply.ja-KL2TLZGJ.js +181 -0
  26. package/dist/dev/vidply.ja-KL2TLZGJ.js.map +7 -0
  27. package/dist/legacy/vidply.js +53 -13
  28. package/dist/legacy/vidply.js.map +2 -2
  29. package/dist/legacy/vidply.min.js +1 -1
  30. package/dist/legacy/vidply.min.meta.json +15 -15
  31. package/dist/prod/vidply.TranscriptManager-DZ2WZU3K.min.js +6 -0
  32. package/dist/prod/vidply.TranscriptManager-E5QHGFIR.min.js +6 -0
  33. package/dist/prod/vidply.chunk-5DWTMWEO.min.js +6 -0
  34. package/dist/prod/vidply.chunk-IBNYTGGM.min.js +6 -0
  35. package/dist/prod/vidply.de-FR3XX54P.min.js +6 -0
  36. package/dist/prod/vidply.de-HGJBCLLE.min.js +6 -0
  37. package/dist/prod/vidply.es-3IJCQLJ7.min.js +6 -0
  38. package/dist/prod/vidply.es-CZEBXCZN.min.js +6 -0
  39. package/dist/prod/vidply.esm.min.js +4 -4
  40. package/dist/prod/vidply.fr-HFOL7MWA.min.js +6 -0
  41. package/dist/prod/vidply.fr-NC4VEAPH.min.js +6 -0
  42. package/dist/prod/vidply.ja-4ZC6ZQLV.min.js +6 -0
  43. package/dist/prod/vidply.ja-QTVU5C25.min.js +6 -0
  44. package/dist/vidply.esm.min.meta.json +34 -34
  45. package/package.json +1 -1
  46. package/src/controls/TranscriptManager.js +1 -1
  47. package/src/features/PlaylistManager.js +12 -12
  48. package/src/i18n/languages/de.js +9 -1
  49. package/src/i18n/languages/en.js +9 -1
  50. package/src/i18n/languages/es.js +9 -1
  51. package/src/i18n/languages/fr.js +9 -1
  52. package/src/i18n/languages/ja.js +9 -1
@@ -0,0 +1,1744 @@
1
+ /*!
2
+ * Universal, Accessible Video Player
3
+ * (c) 2025 Matthias Peltzer
4
+ * Released under GPL-2.0-or-later License
5
+ */
6
+ import {
7
+ DOMUtils,
8
+ DraggableResizable,
9
+ StorageManager,
10
+ TimeUtils,
11
+ attachMenuKeyboardNavigation,
12
+ createIconElement,
13
+ createLabeledSelect,
14
+ createMenuItem,
15
+ focusElement,
16
+ i18n,
17
+ preventDragOnElement
18
+ } from "./vidply.chunk-5663PYKK.js";
19
+
20
+ // src/controls/TranscriptManager.js
21
+ var TranscriptManager = class {
22
+ constructor(player) {
23
+ this.player = player;
24
+ this.transcriptWindow = null;
25
+ this.transcriptEntries = [];
26
+ this.metadataCues = [];
27
+ this.currentActiveEntry = null;
28
+ this.isVisible = false;
29
+ this.storage = new StorageManager("vidply");
30
+ this.draggableResizable = null;
31
+ this.settingsMenuVisible = false;
32
+ this.settingsMenu = null;
33
+ this.settingsButton = null;
34
+ this.settingsMenuJustOpened = false;
35
+ this.resizeOptionButton = null;
36
+ this.resizeOptionText = null;
37
+ this.dragOptionButton = null;
38
+ this.dragOptionText = null;
39
+ this.resizeModeIndicator = null;
40
+ this.resizeModeIndicatorTimeout = null;
41
+ this.transcriptResizeHandles = [];
42
+ this.liveRegion = null;
43
+ this.styleDialog = null;
44
+ this.styleDialogVisible = false;
45
+ this.styleDialogJustOpened = false;
46
+ this.languageSelector = null;
47
+ this.languageLabel = null;
48
+ this.currentTranscriptLanguage = null;
49
+ this.availableTranscriptLanguages = [];
50
+ this.languageSelectorHandler = null;
51
+ const savedPreferences = this.storage.getTranscriptPreferences();
52
+ this.autoscrollEnabled = savedPreferences?.autoscroll !== void 0 ? savedPreferences.autoscroll : true;
53
+ this.showTimestamps = savedPreferences?.showTimestamps !== void 0 ? savedPreferences.showTimestamps : false;
54
+ this.transcriptStyle = {
55
+ fontSize: savedPreferences?.fontSize || this.player.options.transcriptFontSize || "100%",
56
+ fontFamily: savedPreferences?.fontFamily || this.player.options.transcriptFontFamily || "sans-serif",
57
+ color: savedPreferences?.color || this.player.options.transcriptColor || "#ffffff",
58
+ backgroundColor: savedPreferences?.backgroundColor || this.player.options.transcriptBackgroundColor || "#1e1e1e",
59
+ opacity: savedPreferences?.opacity ?? this.player.options.transcriptOpacity ?? 0.98
60
+ };
61
+ this.handlers = {
62
+ timeupdate: () => this.updateActiveEntry(),
63
+ audiodescriptionenabled: () => {
64
+ if (this.isVisible) {
65
+ this.loadTranscriptData();
66
+ }
67
+ },
68
+ audiodescriptiondisabled: () => {
69
+ if (this.isVisible) {
70
+ this.loadTranscriptData();
71
+ }
72
+ },
73
+ resize: null,
74
+ settingsClick: null,
75
+ settingsKeydown: null,
76
+ documentClick: null,
77
+ styleDialogKeydown: null
78
+ };
79
+ this.timeouts = /* @__PURE__ */ new Set();
80
+ this.init();
81
+ }
82
+ init() {
83
+ this.setupMetadataHandlingOnLoad();
84
+ this.player.on("timeupdate", this.handlers.timeupdate);
85
+ this.player.on("audiodescriptionenabled", this.handlers.audiodescriptionenabled);
86
+ this.player.on("audiodescriptiondisabled", this.handlers.audiodescriptiondisabled);
87
+ this.player.on("fullscreenchange", () => {
88
+ if (this.isVisible) {
89
+ const isMobile = window.innerWidth < 768;
90
+ if (isMobile) {
91
+ this.setupDragAndDrop();
92
+ }
93
+ if (!this.draggableResizable || !this.draggableResizable.manuallyPositioned) {
94
+ this.setManagedTimeout(() => this.positionTranscript(), 100);
95
+ }
96
+ }
97
+ });
98
+ }
99
+ /**
100
+ * Toggle transcript window visibility
101
+ */
102
+ toggleTranscript() {
103
+ if (this.isVisible) {
104
+ this.hideTranscript();
105
+ } else {
106
+ this.showTranscript();
107
+ }
108
+ }
109
+ /**
110
+ * Show transcript window
111
+ */
112
+ showTranscript() {
113
+ if (this.transcriptWindow) {
114
+ this.transcriptWindow.style.display = "flex";
115
+ this.isVisible = true;
116
+ if (this.player.controlBar && typeof this.player.controlBar.updateTranscriptButton === "function") {
117
+ this.player.controlBar.updateTranscriptButton();
118
+ }
119
+ focusElement(this.settingsButton, { delay: 150 });
120
+ return;
121
+ }
122
+ this.createTranscriptWindow();
123
+ this.loadTranscriptData();
124
+ if (this.transcriptWindow) {
125
+ this.transcriptWindow.style.display = "flex";
126
+ if (!this.draggableResizable || !this.draggableResizable.manuallyPositioned) {
127
+ this.setManagedTimeout(() => this.positionTranscript(), 0);
128
+ }
129
+ focusElement(this.settingsButton, { delay: 150 });
130
+ }
131
+ this.isVisible = true;
132
+ }
133
+ /**
134
+ * Hide transcript window
135
+ */
136
+ hideTranscript({ focusButton = false } = {}) {
137
+ if (this.transcriptWindow) {
138
+ this.transcriptWindow.style.display = "none";
139
+ this.isVisible = false;
140
+ }
141
+ if (this.draggableResizable && this.draggableResizable.pointerResizeMode) {
142
+ this.draggableResizable.disablePointerResizeMode();
143
+ this.updateResizeOptionState();
144
+ }
145
+ this.hideResizeModeIndicator();
146
+ this.announceLive("");
147
+ if (this.player.controlBar && typeof this.player.controlBar.updateTranscriptButton === "function") {
148
+ this.player.controlBar.updateTranscriptButton();
149
+ }
150
+ if (focusButton) {
151
+ const transcriptButton = this.player.controlBar?.controls?.transcript;
152
+ if (transcriptButton && typeof transcriptButton.focus === "function") {
153
+ transcriptButton.focus({ preventScroll: true });
154
+ }
155
+ }
156
+ }
157
+ /**
158
+ * Create the transcript window UI
159
+ */
160
+ createTranscriptWindow() {
161
+ this.transcriptWindow = DOMUtils.createElement("div", {
162
+ className: `${this.player.options.classPrefix}-transcript-window`,
163
+ attributes: {
164
+ "role": "dialog",
165
+ "aria-label": "Video Transcript",
166
+ "tabindex": "-1"
167
+ }
168
+ });
169
+ this.transcriptHeader = DOMUtils.createElement("div", {
170
+ className: `${this.player.options.classPrefix}-transcript-header`,
171
+ attributes: {
172
+ "tabindex": "0"
173
+ }
174
+ });
175
+ this.headerLeft = DOMUtils.createElement("div", {
176
+ className: `${this.player.options.classPrefix}-transcript-header-left`
177
+ });
178
+ const settingsAriaLabel = i18n.t("transcript.settingsMenu");
179
+ this.settingsButton = DOMUtils.createElement("button", {
180
+ className: `${this.player.options.classPrefix}-transcript-settings`,
181
+ attributes: {
182
+ "type": "button",
183
+ "aria-label": settingsAriaLabel,
184
+ "aria-expanded": "false"
185
+ }
186
+ });
187
+ this.settingsButton.appendChild(createIconElement("settings"));
188
+ DOMUtils.attachTooltip(this.settingsButton, settingsAriaLabel, this.player.options.classPrefix);
189
+ this.handlers.settingsClick = (e) => {
190
+ e.preventDefault();
191
+ e.stopPropagation();
192
+ if (this.settingsMenuVisible) {
193
+ this.hideSettingsMenu();
194
+ } else {
195
+ this.showSettingsMenu();
196
+ }
197
+ };
198
+ this.settingsButton.addEventListener("click", this.handlers.settingsClick);
199
+ this.handlers.settingsKeydown = (e) => {
200
+ if (e.key === "d" || e.key === "D") {
201
+ e.preventDefault();
202
+ e.stopPropagation();
203
+ this.toggleKeyboardDragMode();
204
+ } else if (e.key === "r" || e.key === "R") {
205
+ e.preventDefault();
206
+ e.stopPropagation();
207
+ this.toggleResizeMode();
208
+ } else if (e.key === "Escape" && this.settingsMenuVisible) {
209
+ e.preventDefault();
210
+ e.stopPropagation();
211
+ this.hideSettingsMenu();
212
+ }
213
+ };
214
+ this.settingsButton.addEventListener("keydown", this.handlers.settingsKeydown);
215
+ const title = DOMUtils.createElement("h3", {
216
+ textContent: `${i18n.t("transcript.title")}. ${i18n.t("transcript.dragResizePrompt")}`
217
+ });
218
+ const autoscrollId = `${this.player.options.classPrefix}-transcript-autoscroll-${Date.now()}`;
219
+ const autoscrollLabel = DOMUtils.createElement("label", {
220
+ className: `${this.player.options.classPrefix}-transcript-autoscroll-label`,
221
+ attributes: {
222
+ "for": autoscrollId
223
+ }
224
+ });
225
+ this.autoscrollCheckbox = DOMUtils.createElement("input", {
226
+ attributes: {
227
+ "id": autoscrollId,
228
+ "type": "checkbox"
229
+ }
230
+ });
231
+ if (this.autoscrollEnabled) {
232
+ this.autoscrollCheckbox.checked = true;
233
+ }
234
+ const autoscrollText = DOMUtils.createElement("span", {
235
+ textContent: i18n.t("transcript.autoscroll"),
236
+ className: `${this.player.options.classPrefix}-transcript-autoscroll-text`
237
+ });
238
+ autoscrollLabel.appendChild(this.autoscrollCheckbox);
239
+ autoscrollLabel.appendChild(autoscrollText);
240
+ this.autoscrollCheckbox.addEventListener("change", (e) => {
241
+ this.autoscrollEnabled = e.target.checked;
242
+ this.saveAutoscrollPreference();
243
+ });
244
+ this.transcriptHeader.appendChild(title);
245
+ this.headerLeft.appendChild(this.settingsButton);
246
+ this.headerLeft.appendChild(autoscrollLabel);
247
+ const selectId = `${this.player.options.classPrefix}-transcript-language-select-${Date.now()}`;
248
+ const { label: languageLabel, select: languageSelector } = createLabeledSelect({
249
+ classPrefix: this.player.options.classPrefix,
250
+ labelClass: `${this.player.options.classPrefix}-transcript-language-label`,
251
+ selectClass: `${this.player.options.classPrefix}-transcript-language-select`,
252
+ labelText: "settings.language",
253
+ selectId,
254
+ hidden: false
255
+ // Don't hide individual elements, we'll hide the wrapper instead
256
+ });
257
+ this.languageLabel = languageLabel;
258
+ this.languageSelector = languageSelector;
259
+ const languageSelectorWrapper = DOMUtils.createElement("div", {
260
+ className: `${this.player.options.classPrefix}-transcript-language-wrapper`,
261
+ attributes: {
262
+ "style": "display: none;"
263
+ // Hidden until we detect multiple languages
264
+ }
265
+ });
266
+ languageSelectorWrapper.appendChild(this.languageLabel);
267
+ languageSelectorWrapper.appendChild(this.languageSelector);
268
+ this.languageSelectorWrapper = languageSelectorWrapper;
269
+ preventDragOnElement(languageSelectorWrapper);
270
+ this.headerLeft.appendChild(languageSelectorWrapper);
271
+ const closeAriaLabel = i18n.t("transcript.close");
272
+ const closeButton = DOMUtils.createElement("button", {
273
+ className: `${this.player.options.classPrefix}-transcript-close`,
274
+ attributes: {
275
+ "type": "button",
276
+ "aria-label": closeAriaLabel
277
+ }
278
+ });
279
+ closeButton.appendChild(createIconElement("close"));
280
+ DOMUtils.attachTooltip(closeButton, closeAriaLabel, this.player.options.classPrefix);
281
+ closeButton.addEventListener("click", () => this.hideTranscript({ focusButton: true }));
282
+ this.transcriptHeader.appendChild(this.headerLeft);
283
+ this.transcriptHeader.appendChild(closeButton);
284
+ this.transcriptContent = DOMUtils.createElement("div", {
285
+ className: `${this.player.options.classPrefix}-transcript-content`
286
+ });
287
+ this.transcriptWindow.appendChild(this.transcriptHeader);
288
+ this.transcriptWindow.appendChild(this.transcriptContent);
289
+ this.createResizeHandles();
290
+ this.liveRegion = DOMUtils.createElement("div", {
291
+ className: "vidply-sr-only",
292
+ attributes: {
293
+ "aria-live": "polite",
294
+ "aria-atomic": "true"
295
+ }
296
+ });
297
+ this.transcriptWindow.appendChild(this.liveRegion);
298
+ this.player.container.appendChild(this.transcriptWindow);
299
+ this.setupDragAndDrop();
300
+ if (!this.draggableResizable || !this.draggableResizable.manuallyPositioned) {
301
+ this.positionTranscript();
302
+ }
303
+ this.handlers.documentClick = (e) => {
304
+ if (this.settingsMenuJustOpened) {
305
+ return;
306
+ }
307
+ if (this.styleDialogJustOpened) {
308
+ return;
309
+ }
310
+ if (this.settingsButton && this.settingsButton.contains(e.target)) {
311
+ return;
312
+ }
313
+ if (this.settingsMenu && this.settingsMenu.contains(e.target)) {
314
+ return;
315
+ }
316
+ if (this.settingsMenuVisible) {
317
+ this.hideSettingsMenu();
318
+ }
319
+ if (this.styleDialogVisible && this.styleDialog && !this.styleDialog.contains(e.target)) {
320
+ this.hideStyleDialog();
321
+ }
322
+ };
323
+ this.documentClickHandlerAdded = false;
324
+ let resizeTimeout;
325
+ this.handlers.resize = () => {
326
+ if (resizeTimeout) {
327
+ this.clearManagedTimeout(resizeTimeout);
328
+ }
329
+ resizeTimeout = this.setManagedTimeout(() => {
330
+ if (!this.draggableResizable || !this.draggableResizable.manuallyPositioned) {
331
+ this.positionTranscript();
332
+ }
333
+ }, 100);
334
+ };
335
+ window.addEventListener("resize", this.handlers.resize);
336
+ }
337
+ createResizeHandles() {
338
+ if (!this.transcriptWindow) return;
339
+ const directions = ["n", "s", "e", "w", "ne", "nw", "se", "sw"];
340
+ this.transcriptResizeHandles = directions.map((direction) => {
341
+ const handle = DOMUtils.createElement("div", {
342
+ className: `${this.player.options.classPrefix}-transcript-resize-handle ${this.player.options.classPrefix}-transcript-resize-${direction}`,
343
+ attributes: {
344
+ "data-direction": direction,
345
+ "data-vidply-managed-resize": "true",
346
+ "aria-hidden": "true"
347
+ }
348
+ });
349
+ handle.style.display = "none";
350
+ this.transcriptWindow.appendChild(handle);
351
+ return handle;
352
+ });
353
+ }
354
+ /**
355
+ * Position transcript window next to video
356
+ */
357
+ positionTranscript() {
358
+ if (!this.transcriptWindow || !this.player.videoWrapper || !this.isVisible) return;
359
+ if (this.draggableResizable && this.draggableResizable.manuallyPositioned) {
360
+ return;
361
+ }
362
+ const isMobile = window.innerWidth < 768;
363
+ const videoRect = this.player.videoWrapper.getBoundingClientRect();
364
+ const isFullscreen = this.player.state.fullscreen;
365
+ if (isMobile && !isFullscreen) {
366
+ this.transcriptWindow.style.position = "relative";
367
+ this.transcriptWindow.style.left = "0";
368
+ this.transcriptWindow.style.right = "0";
369
+ this.transcriptWindow.style.bottom = "auto";
370
+ this.transcriptWindow.style.top = "auto";
371
+ this.transcriptWindow.style.width = "100%";
372
+ this.transcriptWindow.style.maxWidth = "100%";
373
+ this.transcriptWindow.style.maxHeight = "400px";
374
+ this.transcriptWindow.style.height = "auto";
375
+ this.transcriptWindow.style.borderRadius = "0";
376
+ this.transcriptWindow.style.transform = "none";
377
+ this.transcriptWindow.style.border = "none";
378
+ this.transcriptWindow.style.borderTop = "1px solid var(--vidply-border-light)";
379
+ this.transcriptWindow.style.removeProperty("border-right");
380
+ this.transcriptWindow.style.removeProperty("border-bottom");
381
+ this.transcriptWindow.style.removeProperty("border-left");
382
+ this.transcriptWindow.style.removeProperty("border-image");
383
+ this.transcriptWindow.style.removeProperty("border-image-source");
384
+ this.transcriptWindow.style.removeProperty("border-image-slice");
385
+ this.transcriptWindow.style.removeProperty("border-image-width");
386
+ this.transcriptWindow.style.removeProperty("border-image-outset");
387
+ this.transcriptWindow.style.removeProperty("border-image-repeat");
388
+ this.transcriptWindow.style.boxShadow = "none";
389
+ if (this.transcriptHeader) {
390
+ this.transcriptHeader.style.cursor = "default";
391
+ }
392
+ if (this.transcriptWindow.parentNode !== this.player.container) {
393
+ this.player.container.appendChild(this.transcriptWindow);
394
+ }
395
+ } else if (isFullscreen) {
396
+ this.transcriptWindow.style.position = "fixed";
397
+ this.transcriptWindow.style.left = "auto";
398
+ this.transcriptWindow.style.right = "20px";
399
+ this.transcriptWindow.style.bottom = "80px";
400
+ this.transcriptWindow.style.top = "auto";
401
+ this.transcriptWindow.style.maxHeight = "calc(100vh - 180px)";
402
+ this.transcriptWindow.style.height = "auto";
403
+ const fullscreenMinWidth = 260;
404
+ const fullscreenAvailable = Math.max(fullscreenMinWidth, window.innerWidth - 40);
405
+ const fullscreenDesired = parseFloat(this.transcriptWindow.style.width) || 400;
406
+ const fullscreenWidth = Math.max(fullscreenMinWidth, Math.min(fullscreenDesired, fullscreenAvailable));
407
+ this.transcriptWindow.style.width = `${fullscreenWidth}px`;
408
+ this.transcriptWindow.style.maxWidth = "none";
409
+ this.transcriptWindow.style.borderRadius = "8px";
410
+ this.transcriptWindow.style.border = "1px solid var(--vidply-border)";
411
+ this.transcriptWindow.style.removeProperty("border-top");
412
+ this.transcriptWindow.style.removeProperty("border-right");
413
+ this.transcriptWindow.style.removeProperty("border-bottom");
414
+ this.transcriptWindow.style.removeProperty("border-left");
415
+ this.transcriptWindow.style.removeProperty("border-image");
416
+ this.transcriptWindow.style.removeProperty("border-image-source");
417
+ this.transcriptWindow.style.removeProperty("border-image-slice");
418
+ this.transcriptWindow.style.removeProperty("border-image-width");
419
+ this.transcriptWindow.style.removeProperty("border-image-outset");
420
+ this.transcriptWindow.style.removeProperty("border-image-repeat");
421
+ if (this.transcriptHeader) {
422
+ this.transcriptHeader.style.cursor = "move";
423
+ }
424
+ if (this.transcriptWindow.parentNode !== this.player.container) {
425
+ this.player.container.appendChild(this.transcriptWindow);
426
+ }
427
+ } else {
428
+ const transcriptWidth = parseFloat(this.transcriptWindow.style.width) || 400;
429
+ const padding = 20;
430
+ const minWidth = 260;
431
+ const containerRect = this.player.container.getBoundingClientRect();
432
+ const ensureContainerPositioned = () => {
433
+ const computed = window.getComputedStyle(this.player.container);
434
+ if (computed.position === "static") {
435
+ this.player.container.style.position = "relative";
436
+ }
437
+ };
438
+ ensureContainerPositioned();
439
+ const left = videoRect.right - containerRect.left + padding;
440
+ const availableWidth = window.innerWidth - videoRect.right - padding;
441
+ const appliedWidth = Math.max(minWidth, Math.min(transcriptWidth, availableWidth));
442
+ const appliedHeight = videoRect.height;
443
+ this.transcriptWindow.style.position = "absolute";
444
+ this.transcriptWindow.style.left = `${left}px`;
445
+ this.transcriptWindow.style.right = "auto";
446
+ this.transcriptWindow.style.bottom = "auto";
447
+ this.transcriptWindow.style.top = "0";
448
+ this.transcriptWindow.style.height = `${appliedHeight}px`;
449
+ this.transcriptWindow.style.maxHeight = "none";
450
+ this.transcriptWindow.style.width = `${appliedWidth}px`;
451
+ this.transcriptWindow.style.maxWidth = "none";
452
+ this.transcriptWindow.style.borderRadius = "8px";
453
+ this.transcriptWindow.style.border = "1px solid var(--vidply-border)";
454
+ this.transcriptWindow.style.removeProperty("border-top");
455
+ this.transcriptWindow.style.removeProperty("border-right");
456
+ this.transcriptWindow.style.removeProperty("border-bottom");
457
+ this.transcriptWindow.style.removeProperty("border-left");
458
+ this.transcriptWindow.style.removeProperty("border-image");
459
+ this.transcriptWindow.style.removeProperty("border-image-source");
460
+ this.transcriptWindow.style.removeProperty("border-image-slice");
461
+ this.transcriptWindow.style.removeProperty("border-image-width");
462
+ this.transcriptWindow.style.removeProperty("border-image-outset");
463
+ this.transcriptWindow.style.removeProperty("border-image-repeat");
464
+ if (this.transcriptHeader) {
465
+ this.transcriptHeader.style.cursor = "move";
466
+ }
467
+ if (this.transcriptWindow.parentNode !== this.player.container) {
468
+ this.player.container.appendChild(this.transcriptWindow);
469
+ }
470
+ }
471
+ }
472
+ /**
473
+ * Get available transcript languages from tracks
474
+ */
475
+ getAvailableTranscriptLanguages() {
476
+ const textTracks = this.player.textTracks;
477
+ const languages = /* @__PURE__ */ new Map();
478
+ textTracks.forEach((track) => {
479
+ if ((track.kind === "captions" || track.kind === "subtitles") && track.language) {
480
+ if (!languages.has(track.language)) {
481
+ languages.set(track.language, {
482
+ language: track.language,
483
+ label: track.label || track.language,
484
+ track
485
+ });
486
+ }
487
+ }
488
+ });
489
+ return Array.from(languages.values());
490
+ }
491
+ /**
492
+ * Update language selector dropdown
493
+ */
494
+ updateLanguageSelector() {
495
+ if (!this.languageSelector) return;
496
+ this.availableTranscriptLanguages = this.getAvailableTranscriptLanguages();
497
+ this.languageSelector.innerHTML = "";
498
+ if (this.availableTranscriptLanguages.length < 2) {
499
+ if (this.languageSelectorWrapper) {
500
+ this.languageSelectorWrapper.style.display = "none";
501
+ }
502
+ return;
503
+ }
504
+ if (this.languageSelectorWrapper) {
505
+ this.languageSelectorWrapper.style.display = "flex";
506
+ }
507
+ this.availableTranscriptLanguages.forEach((langInfo, index) => {
508
+ const option = DOMUtils.createElement("option", {
509
+ textContent: langInfo.label,
510
+ attributes: {
511
+ "value": langInfo.language,
512
+ "lang": langInfo.language
513
+ }
514
+ });
515
+ this.languageSelector.appendChild(option);
516
+ });
517
+ if (this.currentTranscriptLanguage) {
518
+ this.languageSelector.value = this.currentTranscriptLanguage;
519
+ } else if (this.availableTranscriptLanguages.length > 0) {
520
+ const activeTrack = this.player.textTracks.find(
521
+ (track) => (track.kind === "captions" || track.kind === "subtitles") && track.mode === "showing"
522
+ );
523
+ this.currentTranscriptLanguage = activeTrack ? activeTrack.language : this.availableTranscriptLanguages[0].language;
524
+ this.languageSelector.value = this.currentTranscriptLanguage;
525
+ }
526
+ if (this.languageSelectorHandler) {
527
+ this.languageSelector.removeEventListener("change", this.languageSelectorHandler);
528
+ }
529
+ this.languageSelectorHandler = (e) => {
530
+ this.currentTranscriptLanguage = e.target.value;
531
+ this.loadTranscriptData();
532
+ if (this.transcriptContent && this.currentTranscriptLanguage) {
533
+ this.transcriptContent.setAttribute("lang", this.currentTranscriptLanguage);
534
+ }
535
+ };
536
+ this.languageSelector.addEventListener("change", this.languageSelectorHandler);
537
+ }
538
+ /**
539
+ * Load transcript data from caption/subtitle tracks
540
+ */
541
+ loadTranscriptData() {
542
+ this.transcriptEntries = [];
543
+ this.transcriptContent.innerHTML = "";
544
+ const textTracks = this.player.textTracks;
545
+ let captionTrack = null;
546
+ if (this.currentTranscriptLanguage) {
547
+ captionTrack = textTracks.find(
548
+ (track) => (track.kind === "captions" || track.kind === "subtitles") && track.language === this.currentTranscriptLanguage
549
+ );
550
+ }
551
+ if (!captionTrack) {
552
+ captionTrack = textTracks.find(
553
+ (track) => track.kind === "captions" || track.kind === "subtitles"
554
+ );
555
+ if (captionTrack) {
556
+ this.currentTranscriptLanguage = captionTrack.language;
557
+ }
558
+ }
559
+ let descriptionTrack = null;
560
+ if (this.currentTranscriptLanguage) {
561
+ descriptionTrack = textTracks.find(
562
+ (track) => track.kind === "descriptions" && track.language === this.currentTranscriptLanguage
563
+ );
564
+ }
565
+ if (!descriptionTrack) {
566
+ descriptionTrack = textTracks.find((track) => track.kind === "descriptions");
567
+ }
568
+ const metadataTrack = textTracks.find((track) => track.kind === "metadata");
569
+ const hasDescriptionTrack = descriptionTrack && this.player.state.audioDescriptionEnabled;
570
+ if (!captionTrack && !hasDescriptionTrack && !metadataTrack) {
571
+ this.showNoTranscriptMessage();
572
+ return;
573
+ }
574
+ const tracksToLoad = [captionTrack, descriptionTrack, metadataTrack].filter(Boolean);
575
+ tracksToLoad.forEach((track) => {
576
+ if (track.mode === "disabled") {
577
+ track.mode = "hidden";
578
+ }
579
+ });
580
+ const needsLoading = tracksToLoad.some((track) => !track.cues || track.cues.length === 0);
581
+ if (needsLoading) {
582
+ const loadingMessage = DOMUtils.createElement("div", {
583
+ className: `${this.player.options.classPrefix}-transcript-loading`,
584
+ textContent: i18n.t("transcript.loading")
585
+ });
586
+ this.transcriptContent.appendChild(loadingMessage);
587
+ let loaded = 0;
588
+ const onLoad = () => {
589
+ loaded++;
590
+ if (loaded >= tracksToLoad.length) {
591
+ this.loadTranscriptData();
592
+ }
593
+ };
594
+ tracksToLoad.forEach((track) => {
595
+ track.addEventListener("load", onLoad, { once: true });
596
+ });
597
+ this.setManagedTimeout(() => {
598
+ this.loadTranscriptData();
599
+ }, 500);
600
+ return;
601
+ }
602
+ const allCues = [];
603
+ if (captionTrack && captionTrack.cues) {
604
+ Array.from(captionTrack.cues).forEach((cue) => {
605
+ allCues.push({ cue, type: "caption" });
606
+ });
607
+ }
608
+ if (descriptionTrack && descriptionTrack.cues && this.player.state.audioDescriptionEnabled) {
609
+ Array.from(descriptionTrack.cues).forEach((cue) => {
610
+ allCues.push({ cue, type: "description" });
611
+ });
612
+ }
613
+ if (metadataTrack && metadataTrack.cues) {
614
+ this.metadataCues = Array.from(metadataTrack.cues);
615
+ this.setupMetadataHandling();
616
+ }
617
+ allCues.sort((a, b) => a.cue.startTime - b.cue.startTime);
618
+ allCues.forEach((item, index) => {
619
+ const entry = this.createTranscriptEntry(item.cue, index, item.type);
620
+ this.transcriptEntries.push({
621
+ element: entry,
622
+ cue: item.cue,
623
+ type: item.type,
624
+ startTime: item.cue.startTime,
625
+ endTime: item.cue.endTime
626
+ });
627
+ this.transcriptContent.appendChild(entry);
628
+ });
629
+ this.applyTranscriptStyles();
630
+ this.updateTimestampVisibility();
631
+ if (this.transcriptContent && this.currentTranscriptLanguage) {
632
+ this.transcriptContent.setAttribute("lang", this.currentTranscriptLanguage);
633
+ }
634
+ this.updateLanguageSelector();
635
+ }
636
+ /**
637
+ * Setup metadata handling on player load
638
+ * This runs independently of transcript loading
639
+ */
640
+ setupMetadataHandlingOnLoad() {
641
+ const setupMetadata = () => {
642
+ const textTracks = this.player.textTracks;
643
+ const metadataTrack = textTracks.find((track) => track.kind === "metadata");
644
+ if (metadataTrack) {
645
+ if (metadataTrack.mode === "disabled") {
646
+ metadataTrack.mode = "hidden";
647
+ }
648
+ if (this.metadataCueChangeHandler) {
649
+ metadataTrack.removeEventListener("cuechange", this.metadataCueChangeHandler);
650
+ }
651
+ this.metadataCueChangeHandler = () => {
652
+ const activeCues = Array.from(metadataTrack.activeCues || []);
653
+ if (activeCues.length > 0) {
654
+ if (this.player.options.debug) {
655
+ console.log("[VidPly Metadata] Active cues:", activeCues.map((c) => ({
656
+ start: c.startTime,
657
+ end: c.endTime,
658
+ text: c.text
659
+ })));
660
+ }
661
+ }
662
+ activeCues.forEach((cue) => {
663
+ this.handleMetadataCue(cue);
664
+ });
665
+ };
666
+ metadataTrack.addEventListener("cuechange", this.metadataCueChangeHandler);
667
+ if (this.player.options.debug) {
668
+ const cueCount = metadataTrack.cues ? metadataTrack.cues.length : 0;
669
+ console.log("[VidPly Metadata] Track enabled,", cueCount, "cues available");
670
+ }
671
+ } else if (this.player.options.debug) {
672
+ console.warn("[VidPly Metadata] No metadata track found");
673
+ }
674
+ };
675
+ setupMetadata();
676
+ this.player.on("loadedmetadata", setupMetadata);
677
+ }
678
+ /**
679
+ * Setup metadata handling
680
+ * Metadata cues are not displayed but can be used programmatically
681
+ * This is called when transcript data is loaded (for storing cues)
682
+ */
683
+ setupMetadataHandling() {
684
+ if (!this.metadataCues || this.metadataCues.length === 0) {
685
+ return;
686
+ }
687
+ if (this.player.options.debug) {
688
+ console.log("[VidPly Metadata]", this.metadataCues.length, "cues stored from transcript load");
689
+ }
690
+ }
691
+ /**
692
+ * Handle individual metadata cues
693
+ * Parses metadata text and emits events or triggers actions
694
+ */
695
+ handleMetadataCue(cue) {
696
+ const text = cue.text.trim();
697
+ if (this.player.options.debug) {
698
+ console.log("[VidPly Metadata] Processing cue:", {
699
+ time: cue.startTime,
700
+ text
701
+ });
702
+ }
703
+ this.player.emit("metadata", {
704
+ time: cue.startTime,
705
+ endTime: cue.endTime,
706
+ text,
707
+ cue
708
+ });
709
+ if (text.includes("PAUSE")) {
710
+ if (!this.player.state.paused) {
711
+ if (this.player.options.debug) {
712
+ console.log("[VidPly Metadata] Pausing video at", cue.startTime);
713
+ }
714
+ this.player.pause();
715
+ }
716
+ this.player.emit("metadata:pause", { time: cue.startTime, text });
717
+ }
718
+ const focusMatch = text.match(/FOCUS:([\w#-]+)/);
719
+ if (focusMatch) {
720
+ const targetSelector = focusMatch[1];
721
+ const targetElement = document.querySelector(targetSelector);
722
+ if (targetElement) {
723
+ if (this.player.options.debug) {
724
+ console.log("[VidPly Metadata] Focusing element:", targetSelector);
725
+ }
726
+ this.setManagedTimeout(() => {
727
+ targetElement.focus({ preventScroll: true });
728
+ }, 10);
729
+ } else if (this.player.options.debug) {
730
+ console.warn("[VidPly Metadata] Element not found:", targetSelector);
731
+ }
732
+ this.player.emit("metadata:focus", {
733
+ time: cue.startTime,
734
+ target: targetSelector,
735
+ element: targetElement,
736
+ text
737
+ });
738
+ }
739
+ const hashtags = text.match(/#[\w-]+/g);
740
+ if (hashtags) {
741
+ if (this.player.options.debug) {
742
+ console.log("[VidPly Metadata] Hashtags found:", hashtags);
743
+ }
744
+ this.player.emit("metadata:hashtags", {
745
+ time: cue.startTime,
746
+ hashtags,
747
+ text
748
+ });
749
+ }
750
+ }
751
+ /**
752
+ * Create a single transcript entry element
753
+ */
754
+ createTranscriptEntry(cue, index, type = "caption") {
755
+ const entryText = this.stripVTTFormatting(cue.text);
756
+ const entry = DOMUtils.createElement("div", {
757
+ className: `${this.player.options.classPrefix}-transcript-entry ${this.player.options.classPrefix}-transcript-${type}`,
758
+ attributes: {
759
+ "tabindex": "0",
760
+ "data-start": String(cue.startTime),
761
+ "data-end": String(cue.endTime),
762
+ "data-type": type
763
+ }
764
+ });
765
+ const timestamp = DOMUtils.createElement("span", {
766
+ className: `${this.player.options.classPrefix}-transcript-time`,
767
+ textContent: TimeUtils.formatTime(cue.startTime),
768
+ attributes: {
769
+ "aria-hidden": "true"
770
+ // Hide from screen readers - decorative timestamp
771
+ }
772
+ });
773
+ const text = DOMUtils.createElement("span", {
774
+ className: `${this.player.options.classPrefix}-transcript-text`,
775
+ textContent: entryText
776
+ });
777
+ entry.appendChild(timestamp);
778
+ entry.appendChild(text);
779
+ const seekToTime = () => {
780
+ this.player.seek(cue.startTime);
781
+ if (this.player.state.paused) {
782
+ this.player.play();
783
+ }
784
+ };
785
+ entry.addEventListener("click", seekToTime);
786
+ entry.addEventListener("keydown", (e) => {
787
+ if (e.key === "Enter" || e.key === " ") {
788
+ e.preventDefault();
789
+ seekToTime();
790
+ }
791
+ });
792
+ return entry;
793
+ }
794
+ /**
795
+ * Strip VTT formatting tags from text
796
+ */
797
+ stripVTTFormatting(text) {
798
+ return text.replace(/<[^>]+>/g, "").replace(/\n/g, " ").trim();
799
+ }
800
+ /**
801
+ * Show message when no transcript is available
802
+ */
803
+ showNoTranscriptMessage() {
804
+ const message = DOMUtils.createElement("div", {
805
+ className: `${this.player.options.classPrefix}-transcript-empty`,
806
+ textContent: i18n.t("transcript.noTranscript")
807
+ });
808
+ this.transcriptContent.appendChild(message);
809
+ }
810
+ /**
811
+ * Update active transcript entry based on current time
812
+ */
813
+ updateActiveEntry() {
814
+ if (!this.isVisible || this.transcriptEntries.length === 0) return;
815
+ const currentTime = this.player.state.currentTime;
816
+ const activeEntry = this.transcriptEntries.find(
817
+ (entry) => currentTime >= entry.startTime && currentTime < entry.endTime
818
+ );
819
+ if (activeEntry && activeEntry !== this.currentActiveEntry) {
820
+ if (this.currentActiveEntry) {
821
+ this.currentActiveEntry.element.classList.remove(
822
+ `${this.player.options.classPrefix}-transcript-entry-active`
823
+ );
824
+ }
825
+ activeEntry.element.classList.add(
826
+ `${this.player.options.classPrefix}-transcript-entry-active`
827
+ );
828
+ this.scrollToEntry(activeEntry.element);
829
+ this.currentActiveEntry = activeEntry;
830
+ } else if (!activeEntry && this.currentActiveEntry) {
831
+ this.currentActiveEntry.element.classList.remove(
832
+ `${this.player.options.classPrefix}-transcript-entry-active`
833
+ );
834
+ this.currentActiveEntry = null;
835
+ }
836
+ }
837
+ /**
838
+ * Scroll transcript window to show active entry
839
+ */
840
+ scrollToEntry(entryElement) {
841
+ if (!this.transcriptContent || !this.autoscrollEnabled) return;
842
+ const contentRect = this.transcriptContent.getBoundingClientRect();
843
+ const entryRect = entryElement.getBoundingClientRect();
844
+ if (entryRect.top < contentRect.top || entryRect.bottom > contentRect.bottom) {
845
+ const scrollTop = entryElement.offsetTop - this.transcriptContent.clientHeight / 2 + entryElement.clientHeight / 2;
846
+ this.transcriptContent.scrollTo({
847
+ top: scrollTop,
848
+ behavior: "smooth"
849
+ });
850
+ }
851
+ }
852
+ /**
853
+ * Save autoscroll preference to localStorage
854
+ */
855
+ saveAutoscrollPreference() {
856
+ const savedPreferences = this.storage.getTranscriptPreferences() || {};
857
+ savedPreferences.autoscroll = this.autoscrollEnabled;
858
+ this.storage.saveTranscriptPreferences(savedPreferences);
859
+ }
860
+ /**
861
+ * Setup drag and drop functionality
862
+ */
863
+ setupDragAndDrop() {
864
+ if (!this.transcriptHeader || !this.transcriptWindow) return;
865
+ const isMobile = window.innerWidth < 768;
866
+ const isFullscreen = this.player.state.fullscreen;
867
+ if (isMobile && !isFullscreen) {
868
+ if (this.draggableResizable) {
869
+ this.draggableResizable.destroy();
870
+ this.draggableResizable = null;
871
+ }
872
+ return;
873
+ }
874
+ if (this.draggableResizable) {
875
+ return;
876
+ }
877
+ this.draggableResizable = new DraggableResizable(this.transcriptWindow, {
878
+ dragHandle: this.transcriptHeader,
879
+ resizeHandles: this.transcriptResizeHandles,
880
+ constrainToViewport: true,
881
+ classPrefix: `${this.player.options.classPrefix}-transcript`,
882
+ keyboardDragKey: "d",
883
+ keyboardResizeKey: "r",
884
+ keyboardStep: 10,
885
+ keyboardStepLarge: 50,
886
+ minWidth: 300,
887
+ minHeight: 200,
888
+ maxWidth: () => Math.max(320, window.innerWidth - 40),
889
+ maxHeight: () => Math.max(200, window.innerHeight - 120),
890
+ pointerResizeIndicatorText: i18n.t("transcript.resizeModeHint"),
891
+ onPointerResizeToggle: (enabled) => {
892
+ this.transcriptResizeHandles.forEach((handle) => {
893
+ handle.style.display = enabled ? "block" : "none";
894
+ });
895
+ this.onPointerResizeModeChange(enabled);
896
+ },
897
+ onDragStart: (e) => {
898
+ const ignoreSelectors = [
899
+ `.${this.player.options.classPrefix}-transcript-close`,
900
+ `.${this.player.options.classPrefix}-transcript-settings`,
901
+ `.${this.player.options.classPrefix}-transcript-language-select`,
902
+ `.${this.player.options.classPrefix}-transcript-language-label`,
903
+ `.${this.player.options.classPrefix}-transcript-settings-menu`,
904
+ `.${this.player.options.classPrefix}-transcript-style-dialog`
905
+ ];
906
+ for (const selector of ignoreSelectors) {
907
+ if (e.target.closest(selector)) {
908
+ return false;
909
+ }
910
+ }
911
+ return true;
912
+ }
913
+ });
914
+ this.customKeyHandler = (e) => {
915
+ const key = e.key.toLowerCase();
916
+ const alreadyPrevented = e.defaultPrevented;
917
+ if (this.settingsMenuVisible || this.styleDialogVisible) {
918
+ return;
919
+ }
920
+ if (key === "home") {
921
+ e.preventDefault();
922
+ e.stopPropagation();
923
+ if (this.draggableResizable) {
924
+ if (this.draggableResizable.pointerResizeMode) {
925
+ this.draggableResizable.disablePointerResizeMode();
926
+ }
927
+ this.draggableResizable.manuallyPositioned = false;
928
+ this.positionTranscript();
929
+ this.updateResizeOptionState();
930
+ this.announceLive(i18n.t("transcript.positionReset"));
931
+ }
932
+ return;
933
+ }
934
+ if (key === "r") {
935
+ if (alreadyPrevented) {
936
+ return;
937
+ }
938
+ e.preventDefault();
939
+ e.stopPropagation();
940
+ const enabled = this.toggleResizeMode();
941
+ if (enabled) {
942
+ this.transcriptWindow.focus({ preventScroll: true });
943
+ }
944
+ return;
945
+ }
946
+ if (key === "escape") {
947
+ if (this.draggableResizable && this.draggableResizable.pointerResizeMode) {
948
+ e.preventDefault();
949
+ e.stopPropagation();
950
+ this.draggableResizable.disablePointerResizeMode();
951
+ return;
952
+ }
953
+ if (this.draggableResizable && this.draggableResizable.keyboardDragMode) {
954
+ e.preventDefault();
955
+ e.stopPropagation();
956
+ this.draggableResizable.disableKeyboardDragMode();
957
+ this.announceLive(i18n.t("transcript.dragModeDisabled"));
958
+ return;
959
+ }
960
+ e.preventDefault();
961
+ e.stopPropagation();
962
+ this.hideTranscript({ focusButton: true });
963
+ return;
964
+ }
965
+ };
966
+ this.transcriptWindow.addEventListener("keydown", this.customKeyHandler);
967
+ }
968
+ /**
969
+ * Toggle keyboard drag mode
970
+ */
971
+ toggleKeyboardDragMode() {
972
+ if (this.draggableResizable) {
973
+ const wasEnabled = this.draggableResizable.keyboardDragMode;
974
+ this.draggableResizable.toggleKeyboardDragMode();
975
+ const isEnabled = this.draggableResizable.keyboardDragMode;
976
+ if (!wasEnabled && isEnabled) {
977
+ this.enableMoveMode();
978
+ }
979
+ this.updateDragOptionState();
980
+ if (this.settingsMenuVisible) {
981
+ this.hideSettingsMenu();
982
+ }
983
+ this.transcriptWindow.focus({ preventScroll: true });
984
+ }
985
+ }
986
+ /**
987
+ * Toggle settings menu visibility
988
+ */
989
+ toggleSettingsMenu() {
990
+ if (this.settingsMenuVisible) {
991
+ this.hideSettingsMenu();
992
+ } else {
993
+ this.showSettingsMenu();
994
+ }
995
+ }
996
+ /**
997
+ * Show settings menu
998
+ */
999
+ showSettingsMenu() {
1000
+ this.settingsMenuJustOpened = true;
1001
+ setTimeout(() => {
1002
+ this.settingsMenuJustOpened = false;
1003
+ }, 350);
1004
+ if (!this.documentClickHandlerAdded) {
1005
+ setTimeout(() => {
1006
+ document.addEventListener("click", this.handlers.documentClick);
1007
+ this.documentClickHandlerAdded = true;
1008
+ }, 300);
1009
+ }
1010
+ if (this.settingsMenu) {
1011
+ this.settingsMenu.style.display = "block";
1012
+ this.settingsMenuVisible = true;
1013
+ if (this.settingsButton) {
1014
+ this.settingsButton.setAttribute("aria-expanded", "true");
1015
+ }
1016
+ this.attachSettingsMenuKeyboardNavigation();
1017
+ this.positionSettingsMenuImmediate();
1018
+ this.updateResizeOptionState();
1019
+ setTimeout(() => {
1020
+ const menuItems = this.settingsMenu.querySelectorAll(`.${this.player.options.classPrefix}-transcript-settings-item`);
1021
+ if (menuItems.length > 0) {
1022
+ menuItems[0].setAttribute("tabindex", "0");
1023
+ for (let i = 1; i < menuItems.length; i++) {
1024
+ menuItems[i].setAttribute("tabindex", "-1");
1025
+ }
1026
+ menuItems[0].focus({ preventScroll: true });
1027
+ }
1028
+ }, 50);
1029
+ return;
1030
+ }
1031
+ this.settingsMenu = DOMUtils.createElement("div", {
1032
+ className: `${this.player.options.classPrefix}-transcript-settings-menu`,
1033
+ attributes: {
1034
+ "role": "menu"
1035
+ }
1036
+ });
1037
+ const keyboardDragOption = createMenuItem({
1038
+ classPrefix: this.player.options.classPrefix,
1039
+ itemClass: `${this.player.options.classPrefix}-transcript-settings-item`,
1040
+ icon: "move",
1041
+ label: "transcript.enableDragMode",
1042
+ hasTextClass: true,
1043
+ onClick: () => {
1044
+ this.toggleKeyboardDragMode();
1045
+ this.hideSettingsMenu();
1046
+ }
1047
+ });
1048
+ keyboardDragOption.setAttribute("role", "switch");
1049
+ keyboardDragOption.setAttribute("aria-checked", "false");
1050
+ const dragTooltip = keyboardDragOption.querySelector(`.${this.player.options.classPrefix}-tooltip`);
1051
+ if (dragTooltip) dragTooltip.remove();
1052
+ const dragButtonText = keyboardDragOption.querySelector(`.${this.player.options.classPrefix}-button-text`);
1053
+ if (dragButtonText) dragButtonText.remove();
1054
+ this.dragOptionButton = keyboardDragOption;
1055
+ this.dragOptionText = keyboardDragOption.querySelector(`.${this.player.options.classPrefix}-settings-text`);
1056
+ this.updateDragOptionState();
1057
+ const styleOption = createMenuItem({
1058
+ classPrefix: this.player.options.classPrefix,
1059
+ itemClass: `${this.player.options.classPrefix}-transcript-settings-item`,
1060
+ icon: "settings",
1061
+ label: "transcript.styleTranscript",
1062
+ onClick: (e) => {
1063
+ e.preventDefault();
1064
+ e.stopPropagation();
1065
+ this.hideSettingsMenu();
1066
+ setTimeout(() => {
1067
+ this.showStyleDialog();
1068
+ }, 50);
1069
+ }
1070
+ });
1071
+ const styleTooltip = styleOption.querySelector(`.${this.player.options.classPrefix}-tooltip`);
1072
+ if (styleTooltip) styleTooltip.remove();
1073
+ const styleButtonText = styleOption.querySelector(`.${this.player.options.classPrefix}-button-text`);
1074
+ if (styleButtonText) styleButtonText.remove();
1075
+ const resizeOption = createMenuItem({
1076
+ classPrefix: this.player.options.classPrefix,
1077
+ itemClass: `${this.player.options.classPrefix}-transcript-settings-item`,
1078
+ icon: "resize",
1079
+ label: "transcript.enableResizeMode",
1080
+ hasTextClass: true,
1081
+ onClick: (event) => {
1082
+ event.preventDefault();
1083
+ event.stopPropagation();
1084
+ const enabled = this.toggleResizeMode({ focus: false });
1085
+ if (enabled) {
1086
+ this.hideSettingsMenu({ focusButton: false });
1087
+ setTimeout(() => {
1088
+ if (this.transcriptWindow) {
1089
+ this.transcriptWindow.focus({ preventScroll: true });
1090
+ }
1091
+ }, 20);
1092
+ } else {
1093
+ this.hideSettingsMenu({ focusButton: true });
1094
+ }
1095
+ }
1096
+ });
1097
+ resizeOption.setAttribute("role", "switch");
1098
+ resizeOption.setAttribute("aria-checked", "false");
1099
+ const resizeTooltip = resizeOption.querySelector(`.${this.player.options.classPrefix}-tooltip`);
1100
+ if (resizeTooltip) resizeTooltip.remove();
1101
+ const resizeButtonText = resizeOption.querySelector(`.${this.player.options.classPrefix}-button-text`);
1102
+ if (resizeButtonText) resizeButtonText.remove();
1103
+ this.resizeOptionButton = resizeOption;
1104
+ this.resizeOptionText = resizeOption.querySelector(`.${this.player.options.classPrefix}-settings-text`);
1105
+ this.updateResizeOptionState();
1106
+ const showTimestampsOption = createMenuItem({
1107
+ classPrefix: this.player.options.classPrefix,
1108
+ itemClass: `${this.player.options.classPrefix}-transcript-settings-item`,
1109
+ icon: "clock",
1110
+ label: "transcript.showTimestamps",
1111
+ hasTextClass: true,
1112
+ onClick: () => {
1113
+ this.toggleShowTimestamps();
1114
+ }
1115
+ });
1116
+ showTimestampsOption.setAttribute("role", "switch");
1117
+ showTimestampsOption.setAttribute("aria-checked", this.showTimestamps ? "true" : "false");
1118
+ const timestampsTooltip = showTimestampsOption.querySelector(`.${this.player.options.classPrefix}-tooltip`);
1119
+ if (timestampsTooltip) timestampsTooltip.remove();
1120
+ const timestampsButtonText = showTimestampsOption.querySelector(`.${this.player.options.classPrefix}-button-text`);
1121
+ if (timestampsButtonText) timestampsButtonText.remove();
1122
+ this.showTimestampsButton = showTimestampsOption;
1123
+ this.showTimestampsText = showTimestampsOption.querySelector(`.${this.player.options.classPrefix}-settings-text`);
1124
+ this.updateShowTimestampsState();
1125
+ const closeOption = createMenuItem({
1126
+ classPrefix: this.player.options.classPrefix,
1127
+ itemClass: `${this.player.options.classPrefix}-transcript-settings-item`,
1128
+ icon: "close",
1129
+ label: "transcript.closeMenu",
1130
+ onClick: () => {
1131
+ this.hideSettingsMenu();
1132
+ }
1133
+ });
1134
+ const closeTooltip = closeOption.querySelector(`.${this.player.options.classPrefix}-tooltip`);
1135
+ if (closeTooltip) closeTooltip.remove();
1136
+ const closeButtonText = closeOption.querySelector(`.${this.player.options.classPrefix}-button-text`);
1137
+ if (closeButtonText) closeButtonText.remove();
1138
+ this.settingsMenu.appendChild(keyboardDragOption);
1139
+ this.settingsMenu.appendChild(resizeOption);
1140
+ this.settingsMenu.appendChild(styleOption);
1141
+ this.settingsMenu.appendChild(showTimestampsOption);
1142
+ this.settingsMenu.appendChild(closeOption);
1143
+ this.settingsMenu.style.visibility = "hidden";
1144
+ this.settingsMenu.style.display = "block";
1145
+ if (this.settingsButton && this.settingsButton.parentNode) {
1146
+ this.settingsButton.insertAdjacentElement("afterend", this.settingsMenu);
1147
+ } else if (this.headerLeft) {
1148
+ this.headerLeft.appendChild(this.settingsMenu);
1149
+ } else if (this.transcriptHeader) {
1150
+ this.transcriptHeader.appendChild(this.settingsMenu);
1151
+ } else {
1152
+ this.transcriptWindow.appendChild(this.settingsMenu);
1153
+ }
1154
+ this.positionSettingsMenuImmediate();
1155
+ requestAnimationFrame(() => {
1156
+ if (this.settingsMenu) {
1157
+ this.settingsMenu.style.visibility = "visible";
1158
+ }
1159
+ });
1160
+ this.settingsMenuKeyHandler = attachMenuKeyboardNavigation(
1161
+ this.settingsMenu,
1162
+ this.settingsButton,
1163
+ `.${this.player.options.classPrefix}-transcript-settings-item`,
1164
+ () => this.hideSettingsMenu({ focusButton: true })
1165
+ );
1166
+ this.settingsMenuVisible = true;
1167
+ this.settingsMenu.style.display = "block";
1168
+ if (this.settingsButton) {
1169
+ this.settingsButton.setAttribute("aria-expanded", "true");
1170
+ }
1171
+ this.updateResizeOptionState();
1172
+ setTimeout(() => {
1173
+ const menuItems = this.settingsMenu.querySelectorAll(`.${this.player.options.classPrefix}-transcript-settings-item`);
1174
+ if (menuItems.length > 0) {
1175
+ menuItems[0].setAttribute("tabindex", "0");
1176
+ for (let i = 1; i < menuItems.length; i++) {
1177
+ menuItems[i].setAttribute("tabindex", "-1");
1178
+ }
1179
+ menuItems[0].focus({ preventScroll: true });
1180
+ }
1181
+ }, 50);
1182
+ }
1183
+ /**
1184
+ * Position settings menu relative to settings button (immediate/synchronous)
1185
+ */
1186
+ positionSettingsMenuImmediate() {
1187
+ if (!this.settingsMenu || !this.settingsButton) return;
1188
+ const container = this.settingsButton.parentElement;
1189
+ if (!container) return;
1190
+ const buttonRect = this.settingsButton.getBoundingClientRect();
1191
+ const containerRect = container.getBoundingClientRect();
1192
+ const menuRect = this.settingsMenu.getBoundingClientRect();
1193
+ const viewportHeight = window.innerHeight;
1194
+ const buttonLeft = buttonRect.left - containerRect.left;
1195
+ const buttonBottom = buttonRect.bottom - containerRect.top;
1196
+ const buttonTop = buttonRect.top - containerRect.top;
1197
+ const spaceBelow = viewportHeight - buttonRect.bottom;
1198
+ const spaceAbove = buttonRect.top;
1199
+ let menuTop = buttonBottom + 4;
1200
+ if (spaceBelow < menuRect.height + 20 && spaceAbove > spaceBelow) {
1201
+ menuTop = buttonTop - menuRect.height - 4;
1202
+ this.settingsMenu.classList.add("vidply-menu-above");
1203
+ } else {
1204
+ this.settingsMenu.classList.remove("vidply-menu-above");
1205
+ }
1206
+ this.settingsMenu.style.top = `${menuTop}px`;
1207
+ this.settingsMenu.style.left = `${buttonLeft}px`;
1208
+ this.settingsMenu.style.right = "auto";
1209
+ this.settingsMenu.style.bottom = "auto";
1210
+ }
1211
+ /**
1212
+ * Position settings menu relative to settings button (async for repositioning)
1213
+ */
1214
+ positionSettingsMenu() {
1215
+ if (!this.settingsMenu || !this.settingsButton) return;
1216
+ requestAnimationFrame(() => {
1217
+ setTimeout(() => {
1218
+ this.positionSettingsMenuImmediate();
1219
+ }, 10);
1220
+ });
1221
+ }
1222
+ /**
1223
+ * Attach keyboard navigation to settings menu
1224
+ */
1225
+ attachSettingsMenuKeyboardNavigation() {
1226
+ if (!this.settingsMenu) return;
1227
+ if (this.settingsMenuKeyHandler) {
1228
+ this.settingsMenu.removeEventListener("keydown", this.settingsMenuKeyHandler, true);
1229
+ }
1230
+ const handler = attachMenuKeyboardNavigation(
1231
+ this.settingsMenu,
1232
+ this.settingsButton,
1233
+ `.${this.player.options.classPrefix}-transcript-settings-item`,
1234
+ () => this.hideSettingsMenu({ focusButton: true })
1235
+ );
1236
+ this.settingsMenuKeyHandler = handler;
1237
+ }
1238
+ /**
1239
+ * Hide settings menu
1240
+ */
1241
+ hideSettingsMenu({ focusButton = true } = {}) {
1242
+ if (this.settingsMenu) {
1243
+ this.settingsMenu.style.display = "none";
1244
+ this.settingsMenuVisible = false;
1245
+ this.settingsMenuJustOpened = false;
1246
+ if (this.settingsMenuKeyHandler) {
1247
+ this.settingsMenu.removeEventListener("keydown", this.settingsMenuKeyHandler, true);
1248
+ this.settingsMenuKeyHandler = null;
1249
+ }
1250
+ if (this.settingsButton) {
1251
+ this.settingsButton.setAttribute("aria-expanded", "false");
1252
+ if (focusButton) {
1253
+ this.settingsButton.focus({ preventScroll: true });
1254
+ }
1255
+ }
1256
+ }
1257
+ }
1258
+ /**
1259
+ * Enable move mode (gives visual feedback)
1260
+ */
1261
+ enableMoveMode() {
1262
+ this.hideResizeModeIndicator();
1263
+ this.transcriptWindow.classList.add(`${this.player.options.classPrefix}-transcript-move-mode`);
1264
+ const tooltip = DOMUtils.createElement("div", {
1265
+ className: `${this.player.options.classPrefix}-transcript-move-tooltip`,
1266
+ textContent: "Drag with mouse or press D for keyboard drag mode"
1267
+ });
1268
+ this.transcriptHeader.appendChild(tooltip);
1269
+ setTimeout(() => {
1270
+ this.transcriptWindow.classList.remove(`${this.player.options.classPrefix}-transcript-move-mode`);
1271
+ if (tooltip.parentNode) {
1272
+ tooltip.remove();
1273
+ }
1274
+ }, 2e3);
1275
+ }
1276
+ /**
1277
+ * Toggle resize mode
1278
+ */
1279
+ toggleResizeMode({ focus = true } = {}) {
1280
+ if (!this.draggableResizable) {
1281
+ return false;
1282
+ }
1283
+ if (this.draggableResizable.pointerResizeMode) {
1284
+ this.draggableResizable.disablePointerResizeMode({ focus });
1285
+ return false;
1286
+ }
1287
+ this.draggableResizable.enablePointerResizeMode({ focus });
1288
+ return true;
1289
+ }
1290
+ updateDragOptionState() {
1291
+ if (!this.dragOptionButton) {
1292
+ return;
1293
+ }
1294
+ const isEnabled = !!(this.draggableResizable && this.draggableResizable.keyboardDragMode);
1295
+ const text = isEnabled ? i18n.t("transcript.disableDragMode") : i18n.t("transcript.enableDragMode");
1296
+ const ariaLabel = isEnabled ? i18n.t("transcript.disableDragModeAria") : i18n.t("transcript.enableDragModeAria");
1297
+ this.dragOptionButton.setAttribute("aria-checked", isEnabled ? "true" : "false");
1298
+ this.dragOptionButton.setAttribute("aria-label", ariaLabel);
1299
+ if (this.dragOptionText) {
1300
+ this.dragOptionText.textContent = text;
1301
+ }
1302
+ }
1303
+ updateResizeOptionState() {
1304
+ if (!this.resizeOptionButton) {
1305
+ return;
1306
+ }
1307
+ const isEnabled = !!(this.draggableResizable && this.draggableResizable.pointerResizeMode);
1308
+ const text = isEnabled ? i18n.t("transcript.disableResizeMode") : i18n.t("transcript.enableResizeMode");
1309
+ const ariaLabel = isEnabled ? i18n.t("transcript.disableResizeModeAria") : i18n.t("transcript.enableResizeModeAria");
1310
+ this.resizeOptionButton.setAttribute("aria-checked", isEnabled ? "true" : "false");
1311
+ this.resizeOptionButton.setAttribute("aria-label", ariaLabel);
1312
+ if (this.resizeOptionText) {
1313
+ this.resizeOptionText.textContent = text;
1314
+ }
1315
+ }
1316
+ toggleShowTimestamps() {
1317
+ this.showTimestamps = !this.showTimestamps;
1318
+ this.updateShowTimestampsState();
1319
+ this.updateTimestampVisibility();
1320
+ this.saveTimestampsPreference();
1321
+ }
1322
+ updateShowTimestampsState() {
1323
+ if (!this.showTimestampsButton) {
1324
+ return;
1325
+ }
1326
+ const text = this.showTimestamps ? i18n.t("transcript.hideTimestamps") : i18n.t("transcript.showTimestamps");
1327
+ const ariaLabel = this.showTimestamps ? i18n.t("transcript.hideTimestampsAria") : i18n.t("transcript.showTimestampsAria");
1328
+ this.showTimestampsButton.setAttribute("aria-checked", this.showTimestamps ? "true" : "false");
1329
+ this.showTimestampsButton.setAttribute("aria-label", ariaLabel);
1330
+ if (this.showTimestampsText) {
1331
+ this.showTimestampsText.textContent = text;
1332
+ }
1333
+ }
1334
+ updateTimestampVisibility() {
1335
+ if (!this.transcriptContent) return;
1336
+ const timestamps = this.transcriptContent.querySelectorAll(`.${this.player.options.classPrefix}-transcript-time`);
1337
+ timestamps.forEach((timestamp) => {
1338
+ timestamp.style.display = this.showTimestamps ? "" : "none";
1339
+ });
1340
+ }
1341
+ saveTimestampsPreference() {
1342
+ const savedPreferences = this.storage.getTranscriptPreferences() || {};
1343
+ savedPreferences.showTimestamps = this.showTimestamps;
1344
+ this.storage.saveTranscriptPreferences(savedPreferences);
1345
+ }
1346
+ showResizeModeIndicator() {
1347
+ if (!this.transcriptHeader) {
1348
+ return;
1349
+ }
1350
+ this.hideResizeModeIndicator();
1351
+ const indicator = DOMUtils.createElement("div", {
1352
+ className: `${this.player.options.classPrefix}-transcript-resize-tooltip`,
1353
+ textContent: i18n.t("transcript.resizeModeHint") || "Resize handles enabled. Drag edges or corners to adjust. Press Esc or R to exit."
1354
+ });
1355
+ this.transcriptHeader.appendChild(indicator);
1356
+ this.resizeModeIndicator = indicator;
1357
+ this.resizeModeIndicatorTimeout = this.setManagedTimeout(() => {
1358
+ this.hideResizeModeIndicator();
1359
+ }, 3e3);
1360
+ }
1361
+ hideResizeModeIndicator() {
1362
+ if (this.resizeModeIndicatorTimeout) {
1363
+ this.clearManagedTimeout(this.resizeModeIndicatorTimeout);
1364
+ this.resizeModeIndicatorTimeout = null;
1365
+ }
1366
+ if (this.resizeModeIndicator && this.resizeModeIndicator.parentNode) {
1367
+ this.resizeModeIndicator.remove();
1368
+ }
1369
+ this.resizeModeIndicator = null;
1370
+ }
1371
+ onPointerResizeModeChange(enabled) {
1372
+ this.updateResizeOptionState();
1373
+ if (enabled) {
1374
+ this.showResizeModeIndicator();
1375
+ this.announceLive(i18n.t("transcript.resizeModeEnabled"));
1376
+ } else {
1377
+ this.hideResizeModeIndicator();
1378
+ this.announceLive(i18n.t("transcript.resizeModeDisabled"));
1379
+ }
1380
+ }
1381
+ /**
1382
+ * Show style dialog
1383
+ */
1384
+ showStyleDialog() {
1385
+ if (this.styleDialog) {
1386
+ this.styleDialog.style.display = "block";
1387
+ this.styleDialogVisible = true;
1388
+ if (this.handlers.styleDialogKeydown) {
1389
+ document.addEventListener("keydown", this.handlers.styleDialogKeydown);
1390
+ }
1391
+ this.styleDialogJustOpened = true;
1392
+ setTimeout(() => {
1393
+ this.styleDialogJustOpened = false;
1394
+ }, 350);
1395
+ setTimeout(() => {
1396
+ const firstSelect = this.styleDialog.querySelector("select, input");
1397
+ if (firstSelect) {
1398
+ firstSelect.focus({ preventScroll: true });
1399
+ }
1400
+ }, 0);
1401
+ return;
1402
+ }
1403
+ this.styleDialog = DOMUtils.createElement("div", {
1404
+ className: `${this.player.options.classPrefix}-transcript-style-dialog`
1405
+ });
1406
+ const title = DOMUtils.createElement("h4", {
1407
+ textContent: i18n.t("transcript.styleTitle"),
1408
+ className: `${this.player.options.classPrefix}-transcript-style-title`
1409
+ });
1410
+ this.styleDialog.appendChild(title);
1411
+ const fontSizeControl = this.createStyleSelectControl(
1412
+ i18n.t("captions.fontSize"),
1413
+ "fontSize",
1414
+ [
1415
+ { label: i18n.t("fontSizes.small"), value: "90%" },
1416
+ { label: i18n.t("fontSizes.normal"), value: "100%" },
1417
+ { label: i18n.t("fontSizes.large"), value: "110%" },
1418
+ { label: i18n.t("fontSizes.xlarge"), value: "120%" }
1419
+ ]
1420
+ );
1421
+ this.styleDialog.appendChild(fontSizeControl);
1422
+ const fontFamilyControl = this.createStyleSelectControl(
1423
+ i18n.t("captions.fontFamily"),
1424
+ "fontFamily",
1425
+ [
1426
+ { label: i18n.t("fontFamilies.sansSerif"), value: "sans-serif" },
1427
+ { label: i18n.t("fontFamilies.serif"), value: "serif" },
1428
+ { label: i18n.t("fontFamilies.monospace"), value: "monospace" }
1429
+ ]
1430
+ );
1431
+ this.styleDialog.appendChild(fontFamilyControl);
1432
+ const colorControl = this.createStyleColorControl(i18n.t("captions.color"), "color");
1433
+ this.styleDialog.appendChild(colorControl);
1434
+ const bgColorControl = this.createStyleColorControl(i18n.t("captions.backgroundColor"), "backgroundColor");
1435
+ this.styleDialog.appendChild(bgColorControl);
1436
+ const opacityControl = this.createStyleOpacityControl(i18n.t("captions.opacity"), "opacity");
1437
+ this.styleDialog.appendChild(opacityControl);
1438
+ const closeBtn = DOMUtils.createElement("button", {
1439
+ className: `${this.player.options.classPrefix}-transcript-style-close`,
1440
+ textContent: i18n.t("settings.close"),
1441
+ attributes: {
1442
+ "type": "button"
1443
+ }
1444
+ });
1445
+ closeBtn.addEventListener("click", () => this.hideStyleDialog());
1446
+ this.styleDialog.appendChild(closeBtn);
1447
+ this.handlers.styleDialogKeydown = (e) => {
1448
+ if (!this.styleDialogVisible) return;
1449
+ if (e.key === "Escape") {
1450
+ e.preventDefault();
1451
+ e.stopPropagation();
1452
+ this.hideStyleDialog();
1453
+ return;
1454
+ }
1455
+ if (e.key === "Tab") {
1456
+ const focusableElements = this.styleDialog.querySelectorAll(
1457
+ "select, input, button"
1458
+ );
1459
+ const firstElement = focusableElements[0];
1460
+ const lastElement = focusableElements[focusableElements.length - 1];
1461
+ if (e.shiftKey && document.activeElement === firstElement) {
1462
+ e.preventDefault();
1463
+ lastElement.focus({ preventScroll: true });
1464
+ } else if (!e.shiftKey && document.activeElement === lastElement) {
1465
+ e.preventDefault();
1466
+ firstElement.focus({ preventScroll: true });
1467
+ }
1468
+ }
1469
+ };
1470
+ document.addEventListener("keydown", this.handlers.styleDialogKeydown);
1471
+ if (this.headerLeft) {
1472
+ this.headerLeft.appendChild(this.styleDialog);
1473
+ } else {
1474
+ this.transcriptHeader.appendChild(this.styleDialog);
1475
+ }
1476
+ this.applyTranscriptStyles();
1477
+ this.styleDialogVisible = true;
1478
+ this.styleDialog.style.display = "block";
1479
+ this.styleDialogJustOpened = true;
1480
+ setTimeout(() => {
1481
+ this.styleDialogJustOpened = false;
1482
+ }, 350);
1483
+ setTimeout(() => {
1484
+ const firstSelect = this.styleDialog.querySelector("select, input");
1485
+ if (firstSelect) {
1486
+ firstSelect.focus({ preventScroll: true });
1487
+ }
1488
+ }, 0);
1489
+ }
1490
+ /**
1491
+ * Hide style dialog
1492
+ */
1493
+ hideStyleDialog() {
1494
+ if (this.styleDialog) {
1495
+ this.styleDialog.style.display = "none";
1496
+ this.styleDialogVisible = false;
1497
+ if (this.handlers.styleDialogKeydown) {
1498
+ document.removeEventListener("keydown", this.handlers.styleDialogKeydown);
1499
+ }
1500
+ if (this.settingsButton) {
1501
+ this.settingsButton.focus({ preventScroll: true });
1502
+ }
1503
+ }
1504
+ }
1505
+ /**
1506
+ * Create style select control
1507
+ */
1508
+ createStyleSelectControl(label, property, options) {
1509
+ const group = DOMUtils.createElement("div", {
1510
+ className: `${this.player.options.classPrefix}-transcript-style-group`
1511
+ });
1512
+ const controlId = `${this.player.options.classPrefix}-transcript-${property}-${Date.now()}`;
1513
+ const labelEl = DOMUtils.createElement("label", {
1514
+ textContent: label,
1515
+ attributes: {
1516
+ "for": controlId
1517
+ }
1518
+ });
1519
+ group.appendChild(labelEl);
1520
+ const select = DOMUtils.createElement("select", {
1521
+ className: `${this.player.options.classPrefix}-transcript-style-select`,
1522
+ attributes: {
1523
+ "id": controlId
1524
+ }
1525
+ });
1526
+ options.forEach((opt) => {
1527
+ const option = DOMUtils.createElement("option", {
1528
+ textContent: opt.label,
1529
+ attributes: {
1530
+ "value": opt.value
1531
+ }
1532
+ });
1533
+ if (this.transcriptStyle[property] === opt.value) {
1534
+ option.selected = true;
1535
+ }
1536
+ select.appendChild(option);
1537
+ });
1538
+ select.addEventListener("change", (e) => {
1539
+ this.transcriptStyle[property] = e.target.value;
1540
+ this.applyTranscriptStyles();
1541
+ this.savePreferences();
1542
+ });
1543
+ group.appendChild(select);
1544
+ return group;
1545
+ }
1546
+ /**
1547
+ * Create style color control
1548
+ */
1549
+ createStyleColorControl(label, property) {
1550
+ const group = DOMUtils.createElement("div", {
1551
+ className: `${this.player.options.classPrefix}-transcript-style-group`
1552
+ });
1553
+ const controlId = `${this.player.options.classPrefix}-transcript-${property}-${Date.now()}`;
1554
+ const labelEl = DOMUtils.createElement("label", {
1555
+ textContent: label,
1556
+ attributes: {
1557
+ "for": controlId
1558
+ }
1559
+ });
1560
+ group.appendChild(labelEl);
1561
+ const input = DOMUtils.createElement("input", {
1562
+ attributes: {
1563
+ "id": controlId,
1564
+ "type": "color",
1565
+ "value": this.transcriptStyle[property]
1566
+ },
1567
+ className: `${this.player.options.classPrefix}-transcript-style-color`
1568
+ });
1569
+ input.addEventListener("input", (e) => {
1570
+ this.transcriptStyle[property] = e.target.value;
1571
+ this.applyTranscriptStyles();
1572
+ this.savePreferences();
1573
+ });
1574
+ group.appendChild(input);
1575
+ return group;
1576
+ }
1577
+ /**
1578
+ * Create style opacity control
1579
+ */
1580
+ createStyleOpacityControl(label, property) {
1581
+ const group = DOMUtils.createElement("div", {
1582
+ className: `${this.player.options.classPrefix}-transcript-style-group`
1583
+ });
1584
+ const controlId = `${this.player.options.classPrefix}-transcript-${property}-${Date.now()}`;
1585
+ const labelEl = DOMUtils.createElement("label", {
1586
+ textContent: label,
1587
+ attributes: {
1588
+ "for": controlId
1589
+ }
1590
+ });
1591
+ group.appendChild(labelEl);
1592
+ const valueDisplay = DOMUtils.createElement("span", {
1593
+ textContent: Math.round(this.transcriptStyle[property] * 100) + "%",
1594
+ className: `${this.player.options.classPrefix}-transcript-style-value`
1595
+ });
1596
+ const input = DOMUtils.createElement("input", {
1597
+ attributes: {
1598
+ "id": controlId,
1599
+ "type": "range",
1600
+ "min": "0",
1601
+ "max": "1",
1602
+ "step": "0.1",
1603
+ "value": String(this.transcriptStyle[property])
1604
+ },
1605
+ className: `${this.player.options.classPrefix}-transcript-style-range`
1606
+ });
1607
+ input.addEventListener("input", (e) => {
1608
+ const value = parseFloat(e.target.value);
1609
+ this.transcriptStyle[property] = value;
1610
+ valueDisplay.textContent = Math.round(value * 100) + "%";
1611
+ this.applyTranscriptStyles();
1612
+ this.savePreferences();
1613
+ });
1614
+ const inputContainer = DOMUtils.createElement("div", {
1615
+ className: `${this.player.options.classPrefix}-transcript-style-range-container`
1616
+ });
1617
+ inputContainer.appendChild(input);
1618
+ inputContainer.appendChild(valueDisplay);
1619
+ group.appendChild(labelEl);
1620
+ group.appendChild(inputContainer);
1621
+ return group;
1622
+ }
1623
+ /**
1624
+ * Save transcript preferences to localStorage
1625
+ */
1626
+ savePreferences() {
1627
+ this.storage.saveTranscriptPreferences(this.transcriptStyle);
1628
+ }
1629
+ /**
1630
+ * Apply transcript styles
1631
+ */
1632
+ applyTranscriptStyles() {
1633
+ if (!this.transcriptWindow) return;
1634
+ this.transcriptWindow.style.backgroundColor = this.transcriptStyle.backgroundColor;
1635
+ this.transcriptWindow.style.opacity = String(this.transcriptStyle.opacity);
1636
+ if (this.transcriptContent) {
1637
+ this.transcriptContent.style.fontSize = this.transcriptStyle.fontSize;
1638
+ this.transcriptContent.style.fontFamily = this.transcriptStyle.fontFamily;
1639
+ this.transcriptContent.style.color = this.transcriptStyle.color;
1640
+ }
1641
+ const textEntries = this.transcriptWindow.querySelectorAll(`.${this.player.options.classPrefix}-transcript-text`);
1642
+ textEntries.forEach((entry) => {
1643
+ entry.style.fontSize = this.transcriptStyle.fontSize;
1644
+ entry.style.fontFamily = this.transcriptStyle.fontFamily;
1645
+ entry.style.color = this.transcriptStyle.color;
1646
+ });
1647
+ const timeEntries = this.transcriptWindow.querySelectorAll(`.${this.player.options.classPrefix}-transcript-time`);
1648
+ timeEntries.forEach((entry) => {
1649
+ entry.style.fontFamily = this.transcriptStyle.fontFamily;
1650
+ });
1651
+ }
1652
+ /**
1653
+ * Set a managed timeout that will be cleaned up on destroy
1654
+ * @param {Function} callback - Callback function
1655
+ * @param {number} delay - Delay in milliseconds
1656
+ * @returns {number} Timeout ID
1657
+ */
1658
+ setManagedTimeout(callback, delay) {
1659
+ const timeoutId = setTimeout(() => {
1660
+ this.timeouts.delete(timeoutId);
1661
+ callback();
1662
+ }, delay);
1663
+ this.timeouts.add(timeoutId);
1664
+ return timeoutId;
1665
+ }
1666
+ /**
1667
+ * Clear a managed timeout
1668
+ * @param {number} timeoutId - Timeout ID to clear
1669
+ */
1670
+ clearManagedTimeout(timeoutId) {
1671
+ if (timeoutId) {
1672
+ clearTimeout(timeoutId);
1673
+ this.timeouts.delete(timeoutId);
1674
+ }
1675
+ }
1676
+ /**
1677
+ * Cleanup
1678
+ */
1679
+ destroy() {
1680
+ this.hideResizeModeIndicator();
1681
+ if (this.draggableResizable) {
1682
+ if (this.draggableResizable.pointerResizeMode) {
1683
+ this.draggableResizable.disablePointerResizeMode();
1684
+ this.updateResizeOptionState();
1685
+ }
1686
+ this.draggableResizable.destroy();
1687
+ this.draggableResizable = null;
1688
+ }
1689
+ if (this.transcriptWindow && this.customKeyHandler) {
1690
+ this.transcriptWindow.removeEventListener("keydown", this.customKeyHandler);
1691
+ this.customKeyHandler = null;
1692
+ }
1693
+ if (this.handlers.timeupdate) {
1694
+ this.player.off("timeupdate", this.handlers.timeupdate);
1695
+ }
1696
+ if (this.handlers.audiodescriptionenabled) {
1697
+ this.player.off("audiodescriptionenabled", this.handlers.audiodescriptionenabled);
1698
+ }
1699
+ if (this.handlers.audiodescriptiondisabled) {
1700
+ this.player.off("audiodescriptiondisabled", this.handlers.audiodescriptiondisabled);
1701
+ }
1702
+ if (this.settingsButton) {
1703
+ if (this.handlers.settingsClick) {
1704
+ this.settingsButton.removeEventListener("click", this.handlers.settingsClick);
1705
+ }
1706
+ if (this.handlers.settingsKeydown) {
1707
+ this.settingsButton.removeEventListener("keydown", this.handlers.settingsKeydown);
1708
+ }
1709
+ }
1710
+ if (this.handlers.styleDialogKeydown) {
1711
+ document.removeEventListener("keydown", this.handlers.styleDialogKeydown);
1712
+ }
1713
+ if (this.handlers.documentClick) {
1714
+ document.removeEventListener("click", this.handlers.documentClick);
1715
+ }
1716
+ if (this.handlers.resize) {
1717
+ window.removeEventListener("resize", this.handlers.resize);
1718
+ }
1719
+ this.timeouts.forEach((timeoutId) => clearTimeout(timeoutId));
1720
+ this.timeouts.clear();
1721
+ this.handlers = null;
1722
+ if (this.transcriptWindow && this.transcriptWindow.parentNode) {
1723
+ this.transcriptWindow.parentNode.removeChild(this.transcriptWindow);
1724
+ }
1725
+ this.transcriptWindow = null;
1726
+ this.transcriptHeader = null;
1727
+ this.transcriptContent = null;
1728
+ this.transcriptEntries = [];
1729
+ this.settingsMenu = null;
1730
+ this.styleDialog = null;
1731
+ this.transcriptResizeHandles = [];
1732
+ this.resizeOptionButton = null;
1733
+ this.resizeOptionText = null;
1734
+ this.liveRegion = null;
1735
+ }
1736
+ announceLive(message) {
1737
+ if (!this.liveRegion) return;
1738
+ this.liveRegion.textContent = message || "";
1739
+ }
1740
+ };
1741
+ export {
1742
+ TranscriptManager
1743
+ };
1744
+ //# sourceMappingURL=vidply.TranscriptManager-UTJBQC5B.js.map