vidply 1.0.6 → 1.0.8

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.
@@ -7,20 +7,60 @@ import { DOMUtils } from '../utils/DOMUtils.js';
7
7
  import { TimeUtils } from '../utils/TimeUtils.js';
8
8
  import { createIconElement } from '../icons/Icons.js';
9
9
  import { i18n } from '../i18n/i18n.js';
10
+ import { StorageManager } from '../utils/StorageManager.js';
10
11
 
11
12
  export class TranscriptManager {
12
13
  constructor(player) {
13
14
  this.player = player;
14
15
  this.transcriptWindow = null;
15
16
  this.transcriptEntries = [];
17
+ this.metadataCues = [];
16
18
  this.currentActiveEntry = null;
17
19
  this.isVisible = false;
18
20
 
21
+ // Storage manager
22
+ this.storage = new StorageManager('vidply');
23
+
19
24
  // Dragging state
20
25
  this.isDragging = false;
21
26
  this.dragOffsetX = 0;
22
27
  this.dragOffsetY = 0;
23
28
 
29
+ // Resizing state
30
+ this.isResizing = false;
31
+ this.resizeDirection = null;
32
+ this.resizeStartX = 0;
33
+ this.resizeStartY = 0;
34
+ this.resizeStartWidth = 0;
35
+ this.resizeStartHeight = 0;
36
+ this.resizeEnabled = false;
37
+
38
+ // Settings menu state
39
+ this.settingsMenuVisible = false;
40
+ this.settingsMenu = null;
41
+ this.settingsButton = null;
42
+ this.settingsMenuJustOpened = false;
43
+
44
+ // Keyboard drag mode
45
+ this.keyboardDragMode = false;
46
+
47
+ // Style dialog state
48
+ this.styleDialog = null;
49
+ this.styleDialogVisible = false;
50
+ this.styleDialogJustOpened = false;
51
+
52
+ // Load saved preferences from localStorage
53
+ const savedPreferences = this.storage.getTranscriptPreferences();
54
+
55
+ // Transcript styling options (with defaults, then player options, then saved preferences)
56
+ this.transcriptStyle = {
57
+ fontSize: savedPreferences?.fontSize || this.player.options.transcriptFontSize || '100%',
58
+ fontFamily: savedPreferences?.fontFamily || this.player.options.transcriptFontFamily || 'sans-serif',
59
+ color: savedPreferences?.color || this.player.options.transcriptColor || '#ffffff',
60
+ backgroundColor: savedPreferences?.backgroundColor || this.player.options.transcriptBackgroundColor || '#1e1e1e',
61
+ opacity: savedPreferences?.opacity ?? this.player.options.transcriptOpacity ?? 0.98
62
+ };
63
+
24
64
  // Store event handlers for cleanup
25
65
  this.handlers = {
26
66
  timeupdate: () => this.updateActiveEntry(),
@@ -31,7 +71,11 @@ export class TranscriptManager {
31
71
  touchend: null,
32
72
  mousedown: null,
33
73
  touchstart: null,
34
- keydown: null
74
+ keydown: null,
75
+ settingsClick: null,
76
+ settingsKeydown: null,
77
+ documentClick: null,
78
+ styleDialogKeydown: null
35
79
  };
36
80
 
37
81
  this.init();
@@ -68,6 +112,13 @@ export class TranscriptManager {
68
112
  if (this.transcriptWindow) {
69
113
  this.transcriptWindow.style.display = 'flex';
70
114
  this.isVisible = true;
115
+
116
+ // Focus the settings button for keyboard accessibility
117
+ setTimeout(() => {
118
+ if (this.settingsButton) {
119
+ this.settingsButton.focus();
120
+ }
121
+ }, 150);
71
122
  return;
72
123
  }
73
124
 
@@ -80,6 +131,13 @@ export class TranscriptManager {
80
131
  this.transcriptWindow.style.display = 'flex';
81
132
  // Re-position after showing (in case window was resized while hidden)
82
133
  setTimeout(() => this.positionTranscript(), 0);
134
+
135
+ // Focus the settings button for keyboard accessibility
136
+ setTimeout(() => {
137
+ if (this.settingsButton) {
138
+ this.settingsButton.focus();
139
+ }
140
+ }, 150);
83
141
  }
84
142
  this.isVisible = true;
85
143
  }
@@ -116,10 +174,62 @@ export class TranscriptManager {
116
174
  }
117
175
  });
118
176
 
177
+ // Header left side (settings button + title)
178
+ this.headerLeft = DOMUtils.createElement('div', {
179
+ className: `${this.player.options.classPrefix}-transcript-header-left`
180
+ });
181
+
182
+ // Settings button
183
+ this.settingsButton = DOMUtils.createElement('button', {
184
+ className: `${this.player.options.classPrefix}-transcript-settings`,
185
+ attributes: {
186
+ 'type': 'button',
187
+ 'aria-label': i18n.t('transcript.settings'),
188
+ 'aria-expanded': 'false'
189
+ }
190
+ });
191
+ this.settingsButton.appendChild(createIconElement('settings'));
192
+ this.handlers.settingsClick = (e) => {
193
+ e.preventDefault();
194
+ e.stopPropagation();
195
+ if (this.settingsMenuVisible) {
196
+ this.hideSettingsMenu();
197
+ } else {
198
+ this.showSettingsMenu();
199
+ }
200
+ };
201
+ this.settingsButton.addEventListener('click', this.handlers.settingsClick);
202
+
203
+ // Keyboard handler for settings button
204
+ this.handlers.settingsKeydown = (e) => {
205
+ // D key to toggle keyboard drag mode
206
+ if (e.key === 'd' || e.key === 'D') {
207
+ e.preventDefault();
208
+ e.stopPropagation();
209
+ this.toggleKeyboardDragMode();
210
+ }
211
+ // R key to toggle resize mode
212
+ else if (e.key === 'r' || e.key === 'R') {
213
+ e.preventDefault();
214
+ e.stopPropagation();
215
+ this.toggleResizeMode();
216
+ }
217
+ // Escape to close menu if open
218
+ else if (e.key === 'Escape' && this.settingsMenuVisible) {
219
+ e.preventDefault();
220
+ e.stopPropagation();
221
+ this.hideSettingsMenu();
222
+ }
223
+ };
224
+ this.settingsButton.addEventListener('keydown', this.handlers.settingsKeydown);
225
+
119
226
  const title = DOMUtils.createElement('h3', {
120
227
  textContent: i18n.t('transcript.title')
121
228
  });
122
229
 
230
+ this.headerLeft.appendChild(this.settingsButton);
231
+ this.headerLeft.appendChild(title);
232
+
123
233
  const closeButton = DOMUtils.createElement('button', {
124
234
  className: `${this.player.options.classPrefix}-transcript-close`,
125
235
  attributes: {
@@ -130,7 +240,7 @@ export class TranscriptManager {
130
240
  closeButton.appendChild(createIconElement('close'));
131
241
  closeButton.addEventListener('click', () => this.hideTranscript());
132
242
 
133
- this.transcriptHeader.appendChild(title);
243
+ this.transcriptHeader.appendChild(this.headerLeft);
134
244
  this.transcriptHeader.appendChild(closeButton);
135
245
 
136
246
  // Content container
@@ -150,6 +260,43 @@ export class TranscriptManager {
150
260
  // Setup drag functionality
151
261
  this.setupDragAndDrop();
152
262
 
263
+ // Setup document click handler to close settings menu and style dialog
264
+ // DON'T add it yet - it will be added when the menu is first opened
265
+ this.handlers.documentClick = (e) => {
266
+ // Ignore if menu was just opened (prevents immediate closing)
267
+ if (this.settingsMenuJustOpened) {
268
+ return;
269
+ }
270
+
271
+ // Ignore if style dialog was just opened (prevents immediate closing)
272
+ if (this.styleDialogJustOpened) {
273
+ return;
274
+ }
275
+
276
+ // Ignore clicks on the settings button itself
277
+ if (this.settingsButton && this.settingsButton.contains(e.target)) {
278
+ return;
279
+ }
280
+
281
+ // Ignore clicks on the settings menu items
282
+ if (this.settingsMenu && this.settingsMenu.contains(e.target)) {
283
+ return;
284
+ }
285
+
286
+ // Close settings menu if clicking outside
287
+ if (this.settingsMenuVisible) {
288
+ this.hideSettingsMenu();
289
+ }
290
+
291
+ // Close style dialog if clicking outside (but not on settings button)
292
+ if (this.styleDialogVisible && this.styleDialog &&
293
+ !this.styleDialog.contains(e.target)) {
294
+ this.hideStyleDialog();
295
+ }
296
+ };
297
+ // Store flag to track if handler has been added
298
+ this.documentClickHandlerAdded = false;
299
+
153
300
  // Re-position on window resize (debounced)
154
301
  let resizeTimeout;
155
302
  this.handlers.resize = () => {
@@ -248,23 +395,34 @@ export class TranscriptManager {
248
395
  this.transcriptEntries = [];
249
396
  this.transcriptContent.innerHTML = '';
250
397
 
251
- // Get caption/subtitle tracks
398
+ // Get all text tracks
252
399
  const textTracks = Array.from(this.player.element.textTracks);
253
- const transcriptTrack = textTracks.find(
400
+
401
+ // Find different track types
402
+ const captionTrack = textTracks.find(
254
403
  track => track.kind === 'captions' || track.kind === 'subtitles'
255
404
  );
405
+ const descriptionTrack = textTracks.find(track => track.kind === 'descriptions');
406
+ const metadataTrack = textTracks.find(track => track.kind === 'metadata');
256
407
 
257
- if (!transcriptTrack) {
408
+ // We need at least one track type
409
+ if (!captionTrack && !descriptionTrack && !metadataTrack) {
258
410
  this.showNoTranscriptMessage();
259
411
  return;
260
412
  }
261
413
 
262
- // Enable track to load cues
263
- if (transcriptTrack.mode === 'disabled') {
264
- transcriptTrack.mode = 'hidden';
265
- }
414
+ // Enable all tracks to load cues
415
+ const tracksToLoad = [captionTrack, descriptionTrack, metadataTrack].filter(Boolean);
416
+ tracksToLoad.forEach(track => {
417
+ if (track.mode === 'disabled') {
418
+ track.mode = 'hidden';
419
+ }
420
+ });
266
421
 
267
- if (!transcriptTrack.cues || transcriptTrack.cues.length === 0) {
422
+ // Check if any tracks are still loading
423
+ const needsLoading = tracksToLoad.some(track => !track.cues || track.cues.length === 0);
424
+
425
+ if (needsLoading) {
268
426
  // Wait for cues to load
269
427
  const loadingMessage = DOMUtils.createElement('div', {
270
428
  className: `${this.player.options.classPrefix}-transcript-loading`,
@@ -272,45 +430,142 @@ export class TranscriptManager {
272
430
  });
273
431
  this.transcriptContent.appendChild(loadingMessage);
274
432
 
433
+ let loaded = 0;
275
434
  const onLoad = () => {
276
- this.loadTranscriptData();
435
+ loaded++;
436
+ if (loaded >= tracksToLoad.length) {
437
+ this.loadTranscriptData();
438
+ }
277
439
  };
278
440
 
279
- transcriptTrack.addEventListener('load', onLoad, { once: true });
441
+ tracksToLoad.forEach(track => {
442
+ track.addEventListener('load', onLoad, { once: true });
443
+ });
280
444
 
281
445
  // Fallback timeout
282
446
  setTimeout(() => {
283
- if (transcriptTrack.cues && transcriptTrack.cues.length > 0) {
284
- this.loadTranscriptData();
285
- }
447
+ this.loadTranscriptData();
286
448
  }, 500);
287
449
 
288
450
  return;
289
451
  }
290
452
 
291
- // Build transcript from cues
292
- const cues = Array.from(transcriptTrack.cues);
293
- cues.forEach((cue, index) => {
294
- const entry = this.createTranscriptEntry(cue, index);
453
+ // Collect all cues from all tracks with their type
454
+ const allCues = [];
455
+
456
+ if (captionTrack && captionTrack.cues) {
457
+ Array.from(captionTrack.cues).forEach(cue => {
458
+ allCues.push({ cue, type: 'caption' });
459
+ });
460
+ }
461
+
462
+ if (descriptionTrack && descriptionTrack.cues) {
463
+ Array.from(descriptionTrack.cues).forEach(cue => {
464
+ allCues.push({ cue, type: 'description' });
465
+ });
466
+ }
467
+
468
+ // Store metadata separately for programmatic use (don't display in transcript)
469
+ if (metadataTrack && metadataTrack.cues) {
470
+ this.metadataCues = Array.from(metadataTrack.cues);
471
+ this.setupMetadataHandling();
472
+ }
473
+
474
+ // Sort all cues by start time
475
+ allCues.sort((a, b) => a.cue.startTime - b.cue.startTime);
476
+
477
+ // Build transcript from captions and descriptions only
478
+ allCues.forEach((item, index) => {
479
+ const entry = this.createTranscriptEntry(item.cue, index, item.type);
295
480
  this.transcriptEntries.push({
296
481
  element: entry,
297
- cue: cue,
298
- startTime: cue.startTime,
299
- endTime: cue.endTime
482
+ cue: item.cue,
483
+ type: item.type,
484
+ startTime: item.cue.startTime,
485
+ endTime: item.cue.endTime
300
486
  });
301
487
  this.transcriptContent.appendChild(entry);
302
488
  });
489
+
490
+ // Apply current styles to newly loaded entries
491
+ this.applyTranscriptStyles();
492
+ }
493
+
494
+ /**
495
+ * Setup metadata handling
496
+ * Metadata cues are not displayed but can be used programmatically
497
+ */
498
+ setupMetadataHandling() {
499
+ if (!this.metadataCues || this.metadataCues.length === 0) {
500
+ return;
501
+ }
502
+
503
+ // Listen for cuechange events on the metadata track to trigger custom actions
504
+ const textTracks = Array.from(this.player.element.textTracks);
505
+ const metadataTrack = textTracks.find(track => track.kind === 'metadata');
506
+
507
+ if (metadataTrack) {
508
+ metadataTrack.addEventListener('cuechange', () => {
509
+ const activeCues = Array.from(metadataTrack.activeCues || []);
510
+ activeCues.forEach(cue => {
511
+ this.handleMetadataCue(cue);
512
+ });
513
+ });
514
+ }
515
+ }
516
+
517
+ /**
518
+ * Handle individual metadata cues
519
+ * Parses metadata text and emits events or triggers actions
520
+ */
521
+ handleMetadataCue(cue) {
522
+ const text = cue.text.trim();
523
+
524
+ // Emit a generic metadata event that developers can listen to
525
+ this.player.emit('metadata', {
526
+ time: cue.startTime,
527
+ endTime: cue.endTime,
528
+ text: text,
529
+ cue: cue
530
+ });
531
+
532
+ // Parse for specific commands (examples based on wwa_meta.vtt format)
533
+ if (text.includes('PAUSE')) {
534
+ // Emit pause suggestion event (don't auto-pause, let developer decide)
535
+ this.player.emit('metadata:pause', { time: cue.startTime, text: text });
536
+ }
537
+
538
+ // Parse for focus directives
539
+ const focusMatch = text.match(/FOCUS:([\w#-]+)/);
540
+ if (focusMatch) {
541
+ this.player.emit('metadata:focus', {
542
+ time: cue.startTime,
543
+ target: focusMatch[1],
544
+ text: text
545
+ });
546
+ }
547
+
548
+ // Parse for hashtag references
549
+ const hashtags = text.match(/#[\w-]+/g);
550
+ if (hashtags) {
551
+ this.player.emit('metadata:hashtags', {
552
+ time: cue.startTime,
553
+ hashtags: hashtags,
554
+ text: text
555
+ });
556
+ }
303
557
  }
304
558
 
305
559
  /**
306
560
  * Create a single transcript entry element
307
561
  */
308
- createTranscriptEntry(cue, index) {
562
+ createTranscriptEntry(cue, index, type = 'caption') {
309
563
  const entry = DOMUtils.createElement('div', {
310
- className: `${this.player.options.classPrefix}-transcript-entry`,
564
+ className: `${this.player.options.classPrefix}-transcript-entry ${this.player.options.classPrefix}-transcript-${type}`,
311
565
  attributes: {
312
566
  'data-start': String(cue.startTime),
313
567
  'data-end': String(cue.endTime),
568
+ 'data-type': type,
314
569
  'role': 'button',
315
570
  'tabindex': '0'
316
571
  }
@@ -442,6 +697,21 @@ export class TranscriptManager {
442
697
  return;
443
698
  }
444
699
 
700
+ // Don't drag if clicking on settings button
701
+ if (e.target.closest(`.${this.player.options.classPrefix}-transcript-settings`)) {
702
+ return;
703
+ }
704
+
705
+ // Don't drag if clicking on settings menu
706
+ if (e.target.closest(`.${this.player.options.classPrefix}-transcript-settings-menu`)) {
707
+ return;
708
+ }
709
+
710
+ // Don't drag if clicking on style dialog
711
+ if (e.target.closest(`.${this.player.options.classPrefix}-transcript-style-dialog`)) {
712
+ return;
713
+ }
714
+
445
715
  this.startDragging(e.clientX, e.clientY);
446
716
  e.preventDefault();
447
717
  };
@@ -463,6 +733,21 @@ export class TranscriptManager {
463
733
  return;
464
734
  }
465
735
 
736
+ // Don't drag if touching settings button
737
+ if (e.target.closest(`.${this.player.options.classPrefix}-transcript-settings`)) {
738
+ return;
739
+ }
740
+
741
+ // Don't drag if touching settings menu
742
+ if (e.target.closest(`.${this.player.options.classPrefix}-transcript-settings-menu`)) {
743
+ return;
744
+ }
745
+
746
+ // Don't drag if touching style dialog
747
+ if (e.target.closest(`.${this.player.options.classPrefix}-transcript-style-dialog`)) {
748
+ return;
749
+ }
750
+
466
751
  const isMobile = window.innerWidth < 640;
467
752
  const isFullscreen = this.player.state.fullscreen;
468
753
  const touch = e.touches[0];
@@ -499,65 +784,85 @@ export class TranscriptManager {
499
784
  };
500
785
 
501
786
  this.handlers.keydown = (e) => {
502
- // Check if this is a navigation key
503
- if (!['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', 'Home', 'Escape'].includes(e.key)) {
787
+ // Handle arrow keys only in keyboard drag mode
788
+ if (['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown'].includes(e.key)) {
789
+ if (!this.keyboardDragMode) {
790
+ // Not in drag mode, let other handlers deal with it
791
+ return;
792
+ }
793
+
794
+ // In drag mode - move the window
795
+ e.preventDefault();
796
+ e.stopPropagation();
797
+
798
+ const step = e.shiftKey ? 50 : 10; // Larger steps with Shift key
799
+
800
+ // Get current position
801
+ let currentLeft = parseFloat(this.transcriptWindow.style.left) || 0;
802
+ let currentTop = parseFloat(this.transcriptWindow.style.top) || 0;
803
+
804
+ // If window is still centered with transform, convert to absolute position first
805
+ const computedStyle = window.getComputedStyle(this.transcriptWindow);
806
+ if (computedStyle.transform !== 'none') {
807
+ const rect = this.transcriptWindow.getBoundingClientRect();
808
+ currentLeft = rect.left;
809
+ currentTop = rect.top;
810
+ this.transcriptWindow.style.transform = 'none';
811
+ this.transcriptWindow.style.left = `${currentLeft}px`;
812
+ this.transcriptWindow.style.top = `${currentTop}px`;
813
+ }
814
+
815
+ // Calculate new position based on arrow key
816
+ let newX = currentLeft;
817
+ let newY = currentTop;
818
+
819
+ switch(e.key) {
820
+ case 'ArrowLeft':
821
+ newX -= step;
822
+ break;
823
+ case 'ArrowRight':
824
+ newX += step;
825
+ break;
826
+ case 'ArrowUp':
827
+ newY -= step;
828
+ break;
829
+ case 'ArrowDown':
830
+ newY += step;
831
+ break;
832
+ }
833
+
834
+ // Set new position directly
835
+ this.transcriptWindow.style.left = `${newX}px`;
836
+ this.transcriptWindow.style.top = `${newY}px`;
504
837
  return;
505
838
  }
506
839
 
507
- // Prevent default behavior and stop event from bubbling to transcript entries
508
- e.preventDefault();
509
- e.stopPropagation();
510
-
511
- // Handle special keys first
840
+ // Handle other special keys
512
841
  if (e.key === 'Home') {
842
+ e.preventDefault();
843
+ e.stopPropagation();
513
844
  this.resetPosition();
514
845
  return;
515
846
  }
516
847
 
517
848
  if (e.key === 'Escape') {
518
- this.hideTranscript();
849
+ e.preventDefault();
850
+ e.stopPropagation();
851
+ if (this.styleDialogVisible) {
852
+ // Close style dialog first
853
+ this.hideStyleDialog();
854
+ } else if (this.keyboardDragMode) {
855
+ // Exit drag mode
856
+ this.disableKeyboardDragMode();
857
+ } else if (this.settingsMenuVisible) {
858
+ // Close settings menu
859
+ this.hideSettingsMenu();
860
+ } else {
861
+ // Close transcript
862
+ this.hideTranscript();
863
+ }
519
864
  return;
520
865
  }
521
-
522
- const step = e.shiftKey ? 50 : 10; // Larger steps with Shift key
523
-
524
- // Get current position
525
- let currentLeft = parseFloat(this.transcriptWindow.style.left) || 0;
526
- let currentTop = parseFloat(this.transcriptWindow.style.top) || 0;
527
-
528
- // If window is still centered with transform, convert to absolute position first
529
- const computedStyle = window.getComputedStyle(this.transcriptWindow);
530
- if (computedStyle.transform !== 'none') {
531
- const rect = this.transcriptWindow.getBoundingClientRect();
532
- currentLeft = rect.left;
533
- currentTop = rect.top;
534
- this.transcriptWindow.style.transform = 'none';
535
- this.transcriptWindow.style.left = `${currentLeft}px`;
536
- this.transcriptWindow.style.top = `${currentTop}px`;
537
- }
538
-
539
- // Calculate new position based on arrow key
540
- let newX = currentLeft;
541
- let newY = currentTop;
542
-
543
- switch(e.key) {
544
- case 'ArrowLeft':
545
- newX -= step;
546
- break;
547
- case 'ArrowRight':
548
- newX += step;
549
- break;
550
- case 'ArrowUp':
551
- newY -= step;
552
- break;
553
- case 'ArrowDown':
554
- newY += step;
555
- break;
556
- }
557
-
558
- // Set new position directly
559
- this.transcriptWindow.style.left = `${newX}px`;
560
- this.transcriptWindow.style.top = `${newY}px`;
561
866
  };
562
867
 
563
868
  // Add event listeners using stored handlers
@@ -671,10 +976,760 @@ export class TranscriptManager {
671
976
  this.transcriptWindow.style.transform = 'translate(-50%, -50%)';
672
977
  }
673
978
 
979
+ /**
980
+ * Toggle keyboard drag mode
981
+ */
982
+ toggleKeyboardDragMode() {
983
+ if (this.keyboardDragMode) {
984
+ this.disableKeyboardDragMode();
985
+ } else {
986
+ this.enableKeyboardDragMode();
987
+ }
988
+ }
989
+
990
+ /**
991
+ * Enable keyboard drag mode
992
+ */
993
+ enableKeyboardDragMode() {
994
+ this.keyboardDragMode = true;
995
+ this.transcriptWindow.classList.add(`${this.player.options.classPrefix}-transcript-keyboard-drag`);
996
+
997
+ // Update settings button aria label
998
+ if (this.settingsButton) {
999
+ this.settingsButton.setAttribute('aria-label', 'Keyboard drag mode active. Use arrow keys to move window. Press D or Escape to exit.');
1000
+ }
1001
+
1002
+ // Add visual indicator
1003
+ const indicator = DOMUtils.createElement('div', {
1004
+ className: `${this.player.options.classPrefix}-transcript-drag-indicator`,
1005
+ textContent: i18n.t('transcript.keyboardDragActive')
1006
+ });
1007
+ this.transcriptHeader.appendChild(indicator);
1008
+
1009
+ // Hide settings menu if open
1010
+ if (this.settingsMenuVisible) {
1011
+ this.hideSettingsMenu();
1012
+ }
1013
+
1014
+ // Focus the header for keyboard navigation
1015
+ this.transcriptHeader.focus();
1016
+ }
1017
+
1018
+ /**
1019
+ * Disable keyboard drag mode
1020
+ */
1021
+ disableKeyboardDragMode() {
1022
+ this.keyboardDragMode = false;
1023
+ this.transcriptWindow.classList.remove(`${this.player.options.classPrefix}-transcript-keyboard-drag`);
1024
+
1025
+ // Update settings button aria label
1026
+ if (this.settingsButton) {
1027
+ this.settingsButton.setAttribute('aria-label', 'Transcript settings. Press Enter to open menu, or D to enable drag mode');
1028
+ }
1029
+
1030
+ // Remove visual indicator
1031
+ const indicator = this.transcriptHeader.querySelector(`.${this.player.options.classPrefix}-transcript-drag-indicator`);
1032
+ if (indicator) {
1033
+ indicator.remove();
1034
+ }
1035
+
1036
+ // Focus back to settings button
1037
+ if (this.settingsButton) {
1038
+ this.settingsButton.focus();
1039
+ }
1040
+ }
1041
+
1042
+ /**
1043
+ * Toggle settings menu visibility
1044
+ */
1045
+ toggleSettingsMenu() {
1046
+ if (this.settingsMenuVisible) {
1047
+ this.hideSettingsMenu();
1048
+ } else {
1049
+ this.showSettingsMenu();
1050
+ }
1051
+ }
1052
+
1053
+ /**
1054
+ * Show settings menu
1055
+ */
1056
+ showSettingsMenu() {
1057
+ // Set flag to prevent immediate closing
1058
+ this.settingsMenuJustOpened = true;
1059
+ setTimeout(() => {
1060
+ this.settingsMenuJustOpened = false;
1061
+ }, 350);
1062
+
1063
+ // Add document click handler on FIRST menu open (not at window creation)
1064
+ if (!this.documentClickHandlerAdded) {
1065
+ setTimeout(() => {
1066
+ document.addEventListener('click', this.handlers.documentClick);
1067
+ this.documentClickHandlerAdded = true;
1068
+ }, 300);
1069
+ }
1070
+
1071
+ if (this.settingsMenu) {
1072
+ this.settingsMenu.style.display = 'block';
1073
+ this.settingsMenuVisible = true;
1074
+ return;
1075
+ }
1076
+ // Create settings menu
1077
+ this.settingsMenu = DOMUtils.createElement('div', {
1078
+ className: `${this.player.options.classPrefix}-transcript-settings-menu`
1079
+ });
1080
+
1081
+ // Keyboard drag option
1082
+ const keyboardDragOption = DOMUtils.createElement('button', {
1083
+ className: `${this.player.options.classPrefix}-transcript-settings-item`,
1084
+ attributes: {
1085
+ 'type': 'button',
1086
+ 'aria-label': i18n.t('transcript.keyboardDragMode')
1087
+ }
1088
+ });
1089
+ const keyboardIcon = createIconElement('move');
1090
+ const keyboardText = DOMUtils.createElement('span', {
1091
+ textContent: i18n.t('transcript.keyboardDragMode')
1092
+ });
1093
+ keyboardDragOption.appendChild(keyboardIcon);
1094
+ keyboardDragOption.appendChild(keyboardText);
1095
+ keyboardDragOption.addEventListener('click', () => {
1096
+ this.toggleKeyboardDragMode();
1097
+ this.hideSettingsMenu();
1098
+ });
1099
+
1100
+ // Style option
1101
+ const styleOption = DOMUtils.createElement('button', {
1102
+ className: `${this.player.options.classPrefix}-transcript-settings-item`,
1103
+ attributes: {
1104
+ 'type': 'button',
1105
+ 'aria-label': i18n.t('transcript.styleTranscript')
1106
+ }
1107
+ });
1108
+ const styleIcon = createIconElement('settings');
1109
+ const styleText = DOMUtils.createElement('span', {
1110
+ textContent: i18n.t('transcript.styleTranscript')
1111
+ });
1112
+ styleOption.appendChild(styleIcon);
1113
+ styleOption.appendChild(styleText);
1114
+ styleOption.addEventListener('click', (e) => {
1115
+ e.preventDefault();
1116
+ e.stopPropagation();
1117
+ this.hideSettingsMenu();
1118
+ // Delay to ensure menu is fully closed before opening dialog
1119
+ setTimeout(() => {
1120
+ this.showStyleDialog();
1121
+ }, 50);
1122
+ });
1123
+
1124
+ // Resize option
1125
+ const resizeOption = DOMUtils.createElement('button', {
1126
+ className: `${this.player.options.classPrefix}-transcript-settings-item`,
1127
+ attributes: {
1128
+ 'type': 'button',
1129
+ 'aria-label': i18n.t('transcript.resizeWindow')
1130
+ }
1131
+ });
1132
+ const resizeIcon = createIconElement('resize');
1133
+ const resizeText = DOMUtils.createElement('span', {
1134
+ textContent: i18n.t('transcript.resizeWindow')
1135
+ });
1136
+ resizeOption.appendChild(resizeIcon);
1137
+ resizeOption.appendChild(resizeText);
1138
+ resizeOption.addEventListener('click', () => {
1139
+ this.toggleResizeMode();
1140
+ this.hideSettingsMenu();
1141
+ });
1142
+
1143
+ // Close option
1144
+ const closeOption = DOMUtils.createElement('button', {
1145
+ className: `${this.player.options.classPrefix}-transcript-settings-item`,
1146
+ attributes: {
1147
+ 'type': 'button',
1148
+ 'aria-label': i18n.t('transcript.closeMenu')
1149
+ }
1150
+ });
1151
+ const closeIcon = createIconElement('close');
1152
+ const closeText = DOMUtils.createElement('span', {
1153
+ textContent: i18n.t('transcript.closeMenu')
1154
+ });
1155
+ closeOption.appendChild(closeIcon);
1156
+ closeOption.appendChild(closeText);
1157
+ closeOption.addEventListener('click', () => {
1158
+ this.hideSettingsMenu();
1159
+ });
1160
+
1161
+ this.settingsMenu.appendChild(keyboardDragOption);
1162
+ this.settingsMenu.appendChild(resizeOption);
1163
+ this.settingsMenu.appendChild(styleOption);
1164
+ this.settingsMenu.appendChild(closeOption);
1165
+
1166
+ // Append menu to header left container for proper positioning
1167
+ if (this.headerLeft) {
1168
+ this.headerLeft.appendChild(this.settingsMenu);
1169
+ } else {
1170
+ this.transcriptHeader.appendChild(this.settingsMenu);
1171
+ }
1172
+
1173
+ // Set the menu as visible and display it
1174
+ this.settingsMenuVisible = true;
1175
+ this.settingsMenu.style.display = 'block';
1176
+
1177
+ // Update aria-expanded
1178
+ if (this.settingsButton) {
1179
+ this.settingsButton.setAttribute('aria-expanded', 'true');
1180
+ }
1181
+
1182
+ // Focus first menu item
1183
+ setTimeout(() => {
1184
+ const firstItem = this.settingsMenu.querySelector(`.${this.player.options.classPrefix}-transcript-settings-item`);
1185
+ if (firstItem) {
1186
+ firstItem.focus();
1187
+ }
1188
+ }, 0);
1189
+ }
1190
+
1191
+ /**
1192
+ * Hide settings menu
1193
+ */
1194
+ hideSettingsMenu() {
1195
+ if (this.settingsMenu) {
1196
+ this.settingsMenu.style.display = 'none';
1197
+ this.settingsMenuVisible = false;
1198
+ this.settingsMenuJustOpened = false;
1199
+
1200
+ // Update aria-expanded
1201
+ if (this.settingsButton) {
1202
+ this.settingsButton.setAttribute('aria-expanded', 'false');
1203
+ // Return focus to settings button
1204
+ this.settingsButton.focus();
1205
+ }
1206
+ }
1207
+ }
1208
+
1209
+ /**
1210
+ * Enable move mode (gives visual feedback)
1211
+ */
1212
+ enableMoveMode() {
1213
+ // Add visual feedback for move mode
1214
+ this.transcriptWindow.classList.add(`${this.player.options.classPrefix}-transcript-move-mode`);
1215
+
1216
+ // Show tooltip about keyboard drag option
1217
+ const tooltip = DOMUtils.createElement('div', {
1218
+ className: `${this.player.options.classPrefix}-transcript-move-tooltip`,
1219
+ textContent: 'Drag with mouse or press D for keyboard drag mode'
1220
+ });
1221
+ this.transcriptHeader.appendChild(tooltip);
1222
+
1223
+ // Remove after 2 seconds
1224
+ setTimeout(() => {
1225
+ this.transcriptWindow.classList.remove(`${this.player.options.classPrefix}-transcript-move-mode`);
1226
+ if (tooltip.parentNode) {
1227
+ tooltip.remove();
1228
+ }
1229
+ }, 2000);
1230
+ }
1231
+
1232
+ /**
1233
+ * Toggle resize mode
1234
+ */
1235
+ toggleResizeMode() {
1236
+ this.resizeEnabled = !this.resizeEnabled;
1237
+
1238
+ if (this.resizeEnabled) {
1239
+ this.enableResizeHandles();
1240
+ } else {
1241
+ this.disableResizeHandles();
1242
+ }
1243
+ }
1244
+
1245
+ /**
1246
+ * Enable resize handles
1247
+ */
1248
+ enableResizeHandles() {
1249
+ if (!this.transcriptWindow) return;
1250
+
1251
+ // Add resize handles if they don't exist
1252
+ const directions = ['n', 's', 'e', 'w', 'ne', 'nw', 'se', 'sw'];
1253
+
1254
+ directions.forEach(direction => {
1255
+ const handle = DOMUtils.createElement('div', {
1256
+ className: `${this.player.options.classPrefix}-transcript-resize-handle ${this.player.options.classPrefix}-transcript-resize-${direction}`,
1257
+ attributes: {
1258
+ 'data-direction': direction
1259
+ }
1260
+ });
1261
+
1262
+ handle.addEventListener('mousedown', (e) => this.startResize(e, direction));
1263
+ handle.addEventListener('touchstart', (e) => this.startResize(e.touches[0], direction));
1264
+
1265
+ this.transcriptWindow.appendChild(handle);
1266
+ });
1267
+
1268
+ this.transcriptWindow.classList.add(`${this.player.options.classPrefix}-transcript-resizable`);
1269
+
1270
+ // Setup resize event handlers
1271
+ this.handlers.resizeMove = (e) => {
1272
+ if (this.isResizing) {
1273
+ this.performResize(e.clientX, e.clientY);
1274
+ }
1275
+ };
1276
+
1277
+ this.handlers.resizeEnd = () => {
1278
+ if (this.isResizing) {
1279
+ this.stopResize();
1280
+ }
1281
+ };
1282
+
1283
+ this.handlers.resizeTouchMove = (e) => {
1284
+ if (this.isResizing) {
1285
+ this.performResize(e.touches[0].clientX, e.touches[0].clientY);
1286
+ e.preventDefault();
1287
+ }
1288
+ };
1289
+
1290
+ document.addEventListener('mousemove', this.handlers.resizeMove);
1291
+ document.addEventListener('mouseup', this.handlers.resizeEnd);
1292
+ document.addEventListener('touchmove', this.handlers.resizeTouchMove);
1293
+ document.addEventListener('touchend', this.handlers.resizeEnd);
1294
+ }
1295
+
1296
+ /**
1297
+ * Disable resize handles
1298
+ */
1299
+ disableResizeHandles() {
1300
+ if (!this.transcriptWindow) return;
1301
+
1302
+ // Remove all resize handles
1303
+ const handles = this.transcriptWindow.querySelectorAll(`.${this.player.options.classPrefix}-transcript-resize-handle`);
1304
+ handles.forEach(handle => handle.remove());
1305
+
1306
+ this.transcriptWindow.classList.remove(`${this.player.options.classPrefix}-transcript-resizable`);
1307
+
1308
+ // Remove resize event handlers
1309
+ if (this.handlers.resizeMove) {
1310
+ document.removeEventListener('mousemove', this.handlers.resizeMove);
1311
+ }
1312
+ if (this.handlers.resizeEnd) {
1313
+ document.removeEventListener('mouseup', this.handlers.resizeEnd);
1314
+ }
1315
+ if (this.handlers.resizeTouchMove) {
1316
+ document.removeEventListener('touchmove', this.handlers.resizeTouchMove);
1317
+ }
1318
+ document.removeEventListener('touchend', this.handlers.resizeEnd);
1319
+ }
1320
+
1321
+ /**
1322
+ * Start resizing
1323
+ */
1324
+ startResize(e, direction) {
1325
+ e.stopPropagation();
1326
+ e.preventDefault();
1327
+
1328
+ this.isResizing = true;
1329
+ this.resizeDirection = direction;
1330
+ this.resizeStartX = e.clientX;
1331
+ this.resizeStartY = e.clientY;
1332
+
1333
+ const rect = this.transcriptWindow.getBoundingClientRect();
1334
+ this.resizeStartWidth = rect.width;
1335
+ this.resizeStartHeight = rect.height;
1336
+
1337
+ this.transcriptWindow.classList.add(`${this.player.options.classPrefix}-transcript-resizing`);
1338
+ document.body.style.cursor = this.getResizeCursor(direction);
1339
+ document.body.style.userSelect = 'none';
1340
+ }
1341
+
1342
+ /**
1343
+ * Perform resize
1344
+ */
1345
+ performResize(clientX, clientY) {
1346
+ if (!this.isResizing) return;
1347
+
1348
+ const deltaX = clientX - this.resizeStartX;
1349
+ const deltaY = clientY - this.resizeStartY;
1350
+
1351
+ let newWidth = this.resizeStartWidth;
1352
+ let newHeight = this.resizeStartHeight;
1353
+
1354
+ const direction = this.resizeDirection;
1355
+
1356
+ // Calculate new dimensions based on direction
1357
+ if (direction.includes('e')) {
1358
+ newWidth = this.resizeStartWidth + deltaX;
1359
+ }
1360
+ if (direction.includes('w')) {
1361
+ newWidth = this.resizeStartWidth - deltaX;
1362
+ }
1363
+ if (direction.includes('s')) {
1364
+ newHeight = this.resizeStartHeight + deltaY;
1365
+ }
1366
+ if (direction.includes('n')) {
1367
+ newHeight = this.resizeStartHeight - deltaY;
1368
+ }
1369
+
1370
+ // Apply minimum and maximum constraints
1371
+ const minWidth = 300;
1372
+ const minHeight = 200;
1373
+ const maxWidth = window.innerWidth - 40;
1374
+ const maxHeight = window.innerHeight - 40;
1375
+
1376
+ newWidth = Math.max(minWidth, Math.min(newWidth, maxWidth));
1377
+ newHeight = Math.max(minHeight, Math.min(newHeight, maxHeight));
1378
+
1379
+ // Apply new dimensions
1380
+ this.transcriptWindow.style.width = `${newWidth}px`;
1381
+ this.transcriptWindow.style.height = `${newHeight}px`;
1382
+ this.transcriptWindow.style.maxWidth = `${newWidth}px`;
1383
+ this.transcriptWindow.style.maxHeight = `${newHeight}px`;
1384
+
1385
+ // Adjust position if resizing from top or left
1386
+ if (direction.includes('w')) {
1387
+ const currentLeft = parseFloat(this.transcriptWindow.style.left) || 0;
1388
+ this.transcriptWindow.style.left = `${currentLeft + (this.resizeStartWidth - newWidth)}px`;
1389
+ }
1390
+ if (direction.includes('n')) {
1391
+ const currentTop = parseFloat(this.transcriptWindow.style.top) || 0;
1392
+ this.transcriptWindow.style.top = `${currentTop + (this.resizeStartHeight - newHeight)}px`;
1393
+ }
1394
+ }
1395
+
1396
+ /**
1397
+ * Stop resizing
1398
+ */
1399
+ stopResize() {
1400
+ this.isResizing = false;
1401
+ this.resizeDirection = null;
1402
+ this.transcriptWindow.classList.remove(`${this.player.options.classPrefix}-transcript-resizing`);
1403
+ document.body.style.cursor = '';
1404
+ document.body.style.userSelect = '';
1405
+ }
1406
+
1407
+ /**
1408
+ * Get cursor style for resize direction
1409
+ */
1410
+ getResizeCursor(direction) {
1411
+ const cursors = {
1412
+ 'n': 'ns-resize',
1413
+ 's': 'ns-resize',
1414
+ 'e': 'ew-resize',
1415
+ 'w': 'ew-resize',
1416
+ 'ne': 'nesw-resize',
1417
+ 'nw': 'nwse-resize',
1418
+ 'se': 'nwse-resize',
1419
+ 'sw': 'nesw-resize'
1420
+ };
1421
+ return cursors[direction] || 'default';
1422
+ }
1423
+
1424
+ /**
1425
+ * Show style dialog
1426
+ */
1427
+ showStyleDialog() {
1428
+ // If dialog already exists, just show it
1429
+ if (this.styleDialog) {
1430
+ this.styleDialog.style.display = 'block';
1431
+ this.styleDialogVisible = true;
1432
+
1433
+ // Set flag to prevent immediate closing from document click
1434
+ this.styleDialogJustOpened = true;
1435
+ setTimeout(() => {
1436
+ this.styleDialogJustOpened = false;
1437
+ }, 350);
1438
+
1439
+ // Focus first control
1440
+ setTimeout(() => {
1441
+ const firstSelect = this.styleDialog.querySelector('select, input');
1442
+ if (firstSelect) {
1443
+ firstSelect.focus();
1444
+ }
1445
+ }, 0);
1446
+ return;
1447
+ }
1448
+
1449
+ // Create style dialog
1450
+ this.styleDialog = DOMUtils.createElement('div', {
1451
+ className: `${this.player.options.classPrefix}-transcript-style-dialog`
1452
+ });
1453
+
1454
+ // Dialog title
1455
+ const title = DOMUtils.createElement('h4', {
1456
+ textContent: i18n.t('transcript.styleTitle'),
1457
+ className: `${this.player.options.classPrefix}-transcript-style-title`
1458
+ });
1459
+ this.styleDialog.appendChild(title);
1460
+
1461
+ // Font Size
1462
+ const fontSizeControl = this.createStyleSelectControl(
1463
+ i18n.t('captions.fontSize'),
1464
+ 'fontSize',
1465
+ [
1466
+ { label: i18n.t('fontSizes.small'), value: '87.5%' },
1467
+ { label: i18n.t('fontSizes.normal'), value: '100%' },
1468
+ { label: i18n.t('fontSizes.large'), value: '125%' },
1469
+ { label: i18n.t('fontSizes.xlarge'), value: '150%' }
1470
+ ]
1471
+ );
1472
+ this.styleDialog.appendChild(fontSizeControl);
1473
+
1474
+ // Font Family
1475
+ const fontFamilyControl = this.createStyleSelectControl(
1476
+ i18n.t('captions.fontFamily'),
1477
+ 'fontFamily',
1478
+ [
1479
+ { label: i18n.t('fontFamilies.sansSerif'), value: 'sans-serif' },
1480
+ { label: i18n.t('fontFamilies.serif'), value: 'serif' },
1481
+ { label: i18n.t('fontFamilies.monospace'), value: 'monospace' }
1482
+ ]
1483
+ );
1484
+ this.styleDialog.appendChild(fontFamilyControl);
1485
+
1486
+ // Text Color
1487
+ const colorControl = this.createStyleColorControl(i18n.t('captions.color'), 'color');
1488
+ this.styleDialog.appendChild(colorControl);
1489
+
1490
+ // Background Color
1491
+ const bgColorControl = this.createStyleColorControl(i18n.t('captions.backgroundColor'), 'backgroundColor');
1492
+ this.styleDialog.appendChild(bgColorControl);
1493
+
1494
+ // Opacity
1495
+ const opacityControl = this.createStyleOpacityControl(i18n.t('captions.opacity'), 'opacity');
1496
+ this.styleDialog.appendChild(opacityControl);
1497
+
1498
+ // Close button
1499
+ const closeBtn = DOMUtils.createElement('button', {
1500
+ className: `${this.player.options.classPrefix}-transcript-style-close`,
1501
+ textContent: i18n.t('settings.close'),
1502
+ attributes: {
1503
+ 'type': 'button'
1504
+ }
1505
+ });
1506
+ closeBtn.addEventListener('click', () => this.hideStyleDialog());
1507
+ this.styleDialog.appendChild(closeBtn);
1508
+
1509
+ // ESC key handler for style dialog
1510
+ this.handlers.styleDialogKeydown = (e) => {
1511
+ if (e.key === 'Escape') {
1512
+ e.preventDefault();
1513
+ e.stopPropagation();
1514
+ this.hideStyleDialog();
1515
+ }
1516
+ };
1517
+ this.styleDialog.addEventListener('keydown', this.handlers.styleDialogKeydown);
1518
+
1519
+ // Append to header left container (same as settings menu) for correct positioning
1520
+ if (this.headerLeft) {
1521
+ this.headerLeft.appendChild(this.styleDialog);
1522
+ } else {
1523
+ this.transcriptHeader.appendChild(this.styleDialog);
1524
+ }
1525
+
1526
+ // Apply current styles
1527
+ this.applyTranscriptStyles();
1528
+
1529
+ // Important: Set visible state and display before focusing
1530
+ this.styleDialogVisible = true;
1531
+ this.styleDialog.style.display = 'block';
1532
+
1533
+ // Set flag to prevent immediate closing from document click
1534
+ this.styleDialogJustOpened = true;
1535
+ setTimeout(() => {
1536
+ this.styleDialogJustOpened = false;
1537
+ }, 350);
1538
+
1539
+ // Focus first control for keyboard accessibility
1540
+ setTimeout(() => {
1541
+ const firstSelect = this.styleDialog.querySelector('select, input');
1542
+ if (firstSelect) {
1543
+ firstSelect.focus();
1544
+ }
1545
+ }, 0);
1546
+ }
1547
+
1548
+ /**
1549
+ * Hide style dialog
1550
+ */
1551
+ hideStyleDialog() {
1552
+ if (this.styleDialog) {
1553
+ this.styleDialog.style.display = 'none';
1554
+ this.styleDialogVisible = false;
1555
+
1556
+ // Return focus to settings button
1557
+ if (this.settingsButton) {
1558
+ this.settingsButton.focus();
1559
+ }
1560
+ }
1561
+ }
1562
+
1563
+ /**
1564
+ * Create style select control
1565
+ */
1566
+ createStyleSelectControl(label, property, options) {
1567
+ const group = DOMUtils.createElement('div', {
1568
+ className: `${this.player.options.classPrefix}-transcript-style-group`
1569
+ });
1570
+
1571
+ const labelEl = DOMUtils.createElement('label', {
1572
+ textContent: label
1573
+ });
1574
+ group.appendChild(labelEl);
1575
+
1576
+ const select = DOMUtils.createElement('select', {
1577
+ className: `${this.player.options.classPrefix}-transcript-style-select`
1578
+ });
1579
+
1580
+ options.forEach(opt => {
1581
+ const option = DOMUtils.createElement('option', {
1582
+ textContent: opt.label,
1583
+ attributes: {
1584
+ 'value': opt.value
1585
+ }
1586
+ });
1587
+ if (this.transcriptStyle[property] === opt.value) {
1588
+ option.selected = true;
1589
+ }
1590
+ select.appendChild(option);
1591
+ });
1592
+
1593
+ select.addEventListener('change', (e) => {
1594
+ this.transcriptStyle[property] = e.target.value;
1595
+ this.applyTranscriptStyles();
1596
+ this.savePreferences();
1597
+ });
1598
+
1599
+ group.appendChild(select);
1600
+ return group;
1601
+ }
1602
+
1603
+ /**
1604
+ * Create style color control
1605
+ */
1606
+ createStyleColorControl(label, property) {
1607
+ const group = DOMUtils.createElement('div', {
1608
+ className: `${this.player.options.classPrefix}-transcript-style-group`
1609
+ });
1610
+
1611
+ const labelEl = DOMUtils.createElement('label', {
1612
+ textContent: label
1613
+ });
1614
+ group.appendChild(labelEl);
1615
+
1616
+ const input = DOMUtils.createElement('input', {
1617
+ attributes: {
1618
+ 'type': 'color',
1619
+ 'value': this.transcriptStyle[property]
1620
+ },
1621
+ className: `${this.player.options.classPrefix}-transcript-style-color`
1622
+ });
1623
+
1624
+ input.addEventListener('input', (e) => {
1625
+ this.transcriptStyle[property] = e.target.value;
1626
+ this.applyTranscriptStyles();
1627
+ this.savePreferences();
1628
+ });
1629
+
1630
+ group.appendChild(input);
1631
+ return group;
1632
+ }
1633
+
1634
+ /**
1635
+ * Create style opacity control
1636
+ */
1637
+ createStyleOpacityControl(label, property) {
1638
+ const group = DOMUtils.createElement('div', {
1639
+ className: `${this.player.options.classPrefix}-transcript-style-group`
1640
+ });
1641
+
1642
+ const labelEl = DOMUtils.createElement('label', {
1643
+ textContent: label
1644
+ });
1645
+ group.appendChild(labelEl);
1646
+
1647
+ const valueDisplay = DOMUtils.createElement('span', {
1648
+ textContent: Math.round(this.transcriptStyle[property] * 100) + '%',
1649
+ className: `${this.player.options.classPrefix}-transcript-style-value`
1650
+ });
1651
+
1652
+ const input = DOMUtils.createElement('input', {
1653
+ attributes: {
1654
+ 'type': 'range',
1655
+ 'min': '0',
1656
+ 'max': '1',
1657
+ 'step': '0.1',
1658
+ 'value': String(this.transcriptStyle[property])
1659
+ },
1660
+ className: `${this.player.options.classPrefix}-transcript-style-range`
1661
+ });
1662
+
1663
+ input.addEventListener('input', (e) => {
1664
+ const value = parseFloat(e.target.value);
1665
+ this.transcriptStyle[property] = value;
1666
+ valueDisplay.textContent = Math.round(value * 100) + '%';
1667
+ this.applyTranscriptStyles();
1668
+ this.savePreferences();
1669
+ });
1670
+
1671
+ const inputContainer = DOMUtils.createElement('div', {
1672
+ className: `${this.player.options.classPrefix}-transcript-style-range-container`
1673
+ });
1674
+ inputContainer.appendChild(input);
1675
+ inputContainer.appendChild(valueDisplay);
1676
+
1677
+ group.appendChild(labelEl);
1678
+ group.appendChild(inputContainer);
1679
+ return group;
1680
+ }
1681
+
1682
+ /**
1683
+ * Save transcript preferences to localStorage
1684
+ */
1685
+ savePreferences() {
1686
+ this.storage.saveTranscriptPreferences(this.transcriptStyle);
1687
+ }
1688
+
1689
+ /**
1690
+ * Apply transcript styles
1691
+ */
1692
+ applyTranscriptStyles() {
1693
+ if (!this.transcriptWindow) return;
1694
+
1695
+ // Apply to transcript window background
1696
+ this.transcriptWindow.style.backgroundColor = this.transcriptStyle.backgroundColor;
1697
+ this.transcriptWindow.style.opacity = String(this.transcriptStyle.opacity);
1698
+
1699
+ // Apply to content area
1700
+ if (this.transcriptContent) {
1701
+ this.transcriptContent.style.fontSize = this.transcriptStyle.fontSize;
1702
+ this.transcriptContent.style.fontFamily = this.transcriptStyle.fontFamily;
1703
+ this.transcriptContent.style.color = this.transcriptStyle.color;
1704
+ }
1705
+
1706
+ // Apply to all text entries (important: override CSS defaults)
1707
+ const textEntries = this.transcriptWindow.querySelectorAll(`.${this.player.options.classPrefix}-transcript-text`);
1708
+ textEntries.forEach(entry => {
1709
+ entry.style.fontSize = this.transcriptStyle.fontSize;
1710
+ entry.style.fontFamily = this.transcriptStyle.fontFamily;
1711
+ entry.style.color = this.transcriptStyle.color;
1712
+ });
1713
+
1714
+ // Apply to timestamp entries as well
1715
+ const timeEntries = this.transcriptWindow.querySelectorAll(`.${this.player.options.classPrefix}-transcript-time`);
1716
+ timeEntries.forEach(entry => {
1717
+ entry.style.fontFamily = this.transcriptStyle.fontFamily;
1718
+ });
1719
+ }
1720
+
674
1721
  /**
675
1722
  * Cleanup
676
1723
  */
677
1724
  destroy() {
1725
+ // Disable modes if active
1726
+ if (this.resizeEnabled) {
1727
+ this.disableResizeHandles();
1728
+ }
1729
+ if (this.keyboardDragMode) {
1730
+ this.disableKeyboardDragMode();
1731
+ }
1732
+
678
1733
  // Remove timeupdate listener from player
679
1734
  if (this.handlers.timeupdate) {
680
1735
  this.player.off('timeupdate', this.handlers.timeupdate);
@@ -692,6 +1747,21 @@ export class TranscriptManager {
692
1747
  this.transcriptHeader.removeEventListener('keydown', this.handlers.keydown);
693
1748
  }
694
1749
  }
1750
+
1751
+ // Remove settings button event listeners
1752
+ if (this.settingsButton) {
1753
+ if (this.handlers.settingsClick) {
1754
+ this.settingsButton.removeEventListener('click', this.handlers.settingsClick);
1755
+ }
1756
+ if (this.handlers.settingsKeydown) {
1757
+ this.settingsButton.removeEventListener('keydown', this.handlers.settingsKeydown);
1758
+ }
1759
+ }
1760
+
1761
+ // Remove style dialog event listeners
1762
+ if (this.styleDialog && this.handlers.styleDialogKeydown) {
1763
+ this.styleDialog.removeEventListener('keydown', this.handlers.styleDialogKeydown);
1764
+ }
695
1765
 
696
1766
  // Remove document-level listeners
697
1767
  if (this.handlers.mousemove) {
@@ -706,6 +1776,9 @@ export class TranscriptManager {
706
1776
  if (this.handlers.touchend) {
707
1777
  document.removeEventListener('touchend', this.handlers.touchend);
708
1778
  }
1779
+ if (this.handlers.documentClick) {
1780
+ document.removeEventListener('click', this.handlers.documentClick);
1781
+ }
709
1782
 
710
1783
  // Remove window-level listeners
711
1784
  if (this.handlers.resize) {
@@ -724,5 +1797,7 @@ export class TranscriptManager {
724
1797
  this.transcriptHeader = null;
725
1798
  this.transcriptContent = null;
726
1799
  this.transcriptEntries = [];
1800
+ this.settingsMenu = null;
1801
+ this.styleDialog = null;
727
1802
  }
728
1803
  }