vidply 1.2.0 → 1.2.3

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 (167) hide show
  1. package/README.md +974 -946
  2. package/dist/dev/{vidply.AudioDescriptionManager-R47Q4YR4.js → vidply.AudioDescriptionManager-JYYLK5KB.js} +275 -19
  3. package/dist/dev/vidply.AudioDescriptionManager-JYYLK5KB.js.map +7 -0
  4. package/dist/dev/{vidply.CaptionStyleMenu-7XHRBWUM.js → vidply.CaptionStyleMenu-47RCKQF5.js} +4 -4
  5. package/dist/dev/{vidply.DASHRenderer-PWEMC3BT.js → vidply.DASHRenderer-VEJ4HDHO.js} +3 -3
  6. package/dist/dev/vidply.DASHRenderer-VEJ4HDHO.js.map +7 -0
  7. package/dist/dev/{vidply.FloatingPlayerManager-PKNNNAS2.js → vidply.FloatingPlayerManager-C2UVRPZY.js} +5 -5
  8. package/dist/dev/vidply.FloatingPlayerManager-C2UVRPZY.js.map +7 -0
  9. package/dist/dev/{vidply.HLSRenderer-E7RQ74HI.js → vidply.HLSRenderer-ZVBK2TNQ.js} +4 -4
  10. package/dist/dev/vidply.HLSRenderer-ZVBK2TNQ.js.map +7 -0
  11. package/dist/dev/vidply.HTML5Renderer-HLRQUOCU.js +12 -0
  12. package/dist/dev/{vidply.SignLanguageManager-FET5FQZ7.js → vidply.SignLanguageManager-FLUCBW5H.js} +8 -8
  13. package/dist/dev/vidply.SignLanguageManager-FLUCBW5H.js.map +7 -0
  14. package/dist/dev/{vidply.SoundCloudRenderer-RVSJZODO.js → vidply.SoundCloudRenderer-MGMAJ7NK.js} +3 -3
  15. package/dist/dev/vidply.SoundCloudRenderer-MGMAJ7NK.js.map +7 -0
  16. package/dist/dev/{vidply.TranscriptManager-5GXOQCLX.js → vidply.TranscriptManager-Y2RYXHJW.js} +10 -10
  17. package/dist/dev/vidply.TranscriptManager-Y2RYXHJW.js.map +7 -0
  18. package/dist/dev/{vidply.VimeoRenderer-ER2LSBBW.js → vidply.VimeoRenderer-RQFQD7OE.js} +3 -3
  19. package/dist/dev/vidply.VimeoRenderer-RQFQD7OE.js.map +7 -0
  20. package/dist/dev/{vidply.YouTubeRenderer-NFTOWJAB.js → vidply.YouTubeRenderer-EU2BARZF.js} +3 -3
  21. package/dist/dev/vidply.YouTubeRenderer-EU2BARZF.js.map +7 -0
  22. package/dist/dev/{vidply.chunk-BNFIUL2Z.js → vidply.chunk-6LYZYI5G.js} +3 -3
  23. package/dist/dev/vidply.chunk-6LYZYI5G.js.map +7 -0
  24. package/dist/dev/{vidply.chunk-NQPURMQY.js → vidply.chunk-A4ESEOQ2.js} +6 -6
  25. package/dist/dev/vidply.chunk-A4ESEOQ2.js.map +7 -0
  26. package/dist/dev/{vidply.chunk-W25HUCHS.js → vidply.chunk-CGYIED2I.js} +2 -2
  27. package/dist/dev/vidply.chunk-CGYIED2I.js.map +7 -0
  28. package/dist/dev/{vidply.chunk-SK4V2DAU.js → vidply.chunk-HZ7UOKOU.js} +3 -3
  29. package/dist/dev/{vidply.chunk-RZWZGDV7.js → vidply.chunk-I6EDRYGJ.js} +33 -6
  30. package/dist/dev/vidply.chunk-I6EDRYGJ.js.map +7 -0
  31. package/dist/dev/{vidply.chunk-64WXMTGL.js → vidply.chunk-JKMQGLCJ.js} +5 -5
  32. package/dist/dev/vidply.chunk-JKMQGLCJ.js.map +7 -0
  33. package/dist/dev/{vidply.chunk-DZSDO3B7.js → vidply.chunk-JZNZX32Z.js} +2 -2
  34. package/dist/dev/{vidply.chunk-VMSQAZM7.js → vidply.chunk-P25MRJ6O.js} +2 -2
  35. package/dist/dev/{vidply.chunk-6RQA4ROD.js → vidply.chunk-PRFNESYA.js} +2 -2
  36. package/dist/dev/{vidply.chunk-KI7C3QVL.js → vidply.chunk-UI3RYCVF.js} +4 -3
  37. package/dist/dev/vidply.chunk-UI3RYCVF.js.map +7 -0
  38. package/dist/dev/{vidply.chunk-JJ47NO24.js → vidply.chunk-UNFGMABP.js} +2 -2
  39. package/dist/dev/vidply.chunk-UNFGMABP.js.map +7 -0
  40. package/dist/dev/{vidply.de-4YAT7FFM.js → vidply.de-VAUVSQXC.js} +29 -2
  41. package/dist/dev/vidply.de-VAUVSQXC.js.map +7 -0
  42. package/dist/dev/{vidply.es-LEWIRUBH.js → vidply.es-G65HFWO6.js} +29 -2
  43. package/dist/dev/vidply.es-G65HFWO6.js.map +7 -0
  44. package/dist/dev/vidply.esm.js +648 -35
  45. package/dist/dev/vidply.esm.js.map +3 -3
  46. package/dist/dev/{vidply.fr-Q4GV2NOE.js → vidply.fr-MJNOZELM.js} +29 -2
  47. package/dist/dev/vidply.fr-MJNOZELM.js.map +7 -0
  48. package/dist/dev/{vidply.ja-S6ITA6WU.js → vidply.ja-INMDHIYJ.js} +29 -2
  49. package/dist/dev/vidply.ja-INMDHIYJ.js.map +7 -0
  50. package/dist/legacy/vidply.js +1062 -34
  51. package/dist/legacy/vidply.js.map +3 -3
  52. package/dist/legacy/vidply.min.js +2 -2
  53. package/dist/legacy/vidply.min.meta.json +127 -55
  54. package/dist/prod/vidply.AudioDescriptionManager-EKWUNOPE.min.js +6 -0
  55. package/dist/prod/{vidply.CaptionStyleMenu-M3KVQWB5.min.js → vidply.CaptionStyleMenu-JTWSNHRQ.min.js} +2 -2
  56. package/dist/prod/{vidply.DASHRenderer-DN3S27AV.min.js → vidply.DASHRenderer-CIDAHFWM.min.js} +2 -2
  57. package/dist/prod/{vidply.FloatingPlayerManager-SKAZ4J43.min.js → vidply.FloatingPlayerManager-VZXHPCPM.min.js} +2 -2
  58. package/dist/prod/{vidply.HLSRenderer-62HH4YTK.min.js → vidply.HLSRenderer-Q6EFYKID.min.js} +2 -2
  59. package/dist/prod/vidply.HTML5Renderer-XRDJUTOO.min.js +6 -0
  60. package/dist/prod/{vidply.SignLanguageManager-V3OBTSSF.min.js → vidply.SignLanguageManager-JUOSA5AM.min.js} +2 -2
  61. package/dist/prod/{vidply.SoundCloudRenderer-CXRVZIAJ.min.js → vidply.SoundCloudRenderer-7ODD46PO.min.js} +2 -2
  62. package/dist/prod/{vidply.TranscriptManager-SYM3DFUY.min.js → vidply.TranscriptManager-4B2QKYGQ.min.js} +2 -2
  63. package/dist/prod/{vidply.VimeoRenderer-72TWRYFQ.min.js → vidply.VimeoRenderer-MR3UYLY7.min.js} +2 -2
  64. package/dist/prod/{vidply.YouTubeRenderer-UMHEV3KD.min.js → vidply.YouTubeRenderer-ITRTC7NR.min.js} +2 -2
  65. package/dist/prod/{vidply.chunk-YEENJS3Q.min.js → vidply.chunk-3EEDXU4T.min.js} +2 -2
  66. package/dist/prod/{vidply.chunk-P4UXOHWA.min.js → vidply.chunk-5HTX5QXW.min.js} +1 -1
  67. package/dist/prod/{vidply.chunk-ABPXVDLZ.min.js → vidply.chunk-6OGHHIYP.min.js} +2 -2
  68. package/dist/prod/{vidply.chunk-UK7YGOB5.min.js → vidply.chunk-GUPGMFUZ.min.js} +2 -2
  69. package/dist/prod/{vidply.chunk-WX3MVEAL.min.js → vidply.chunk-MWEBITCP.min.js} +1 -1
  70. package/dist/prod/{vidply.chunk-2TB6MUHO.min.js → vidply.chunk-PL3P3JHR.min.js} +1 -1
  71. package/dist/prod/{vidply.chunk-EKEYJWMG.min.js → vidply.chunk-QQZGV7TD.min.js} +2 -2
  72. package/dist/prod/{vidply.chunk-Z3YDSA7O.min.js → vidply.chunk-U5IRRYG5.min.js} +1 -1
  73. package/dist/prod/{vidply.chunk-TM6AXJVD.min.js → vidply.chunk-VKHV4QHA.min.js} +2 -2
  74. package/dist/prod/{vidply.chunk-P3PQBY7N.min.js → vidply.chunk-XA4M574A.min.js} +1 -1
  75. package/dist/prod/{vidply.chunk-RI3X7WHK.min.js → vidply.chunk-ZPTV4JEP.min.js} +2 -2
  76. package/dist/prod/{vidply.de-BCHXRQ53.min.js → vidply.de-UZHFCEZU.min.js} +2 -2
  77. package/dist/prod/{vidply.es-PICAKMZD.min.js → vidply.es-BOISUV2S.min.js} +2 -2
  78. package/dist/prod/vidply.esm.min.js +2 -2
  79. package/dist/prod/{vidply.fr-ZILI7CDR.min.js → vidply.fr-ICSMKPFM.min.js} +2 -2
  80. package/dist/prod/{vidply.ja-5WQ4OK4J.min.js → vidply.ja-VQ2HKGAY.min.js} +2 -2
  81. package/dist/vidply.css +5852 -5796
  82. package/dist/vidply.esm.min.meta.json +204 -149
  83. package/dist/vidply.min.css +1 -1
  84. package/package.json +11 -11
  85. package/src/controls/CaptionManager.ts +598 -598
  86. package/src/controls/ControlBar.ts +4191 -4159
  87. package/src/controls/KeyboardHelp.ts +326 -0
  88. package/src/controls/KeyboardManager.ts +387 -383
  89. package/src/controls/SettingsDialog.ts +488 -488
  90. package/src/controls/TranscriptManager.ts +2426 -2426
  91. package/src/core/AudioDescriptionManager.ts +863 -774
  92. package/src/core/DescriptionSpeechManager.ts +232 -0
  93. package/src/core/FloatingPlayerManager.ts +674 -674
  94. package/src/core/LazyInit.ts +80 -80
  95. package/src/core/MediaSessionManager.ts +341 -0
  96. package/src/core/MetadataAlertsManager.ts +514 -514
  97. package/src/core/Player.ts +2973 -2872
  98. package/src/core/PosterManager.ts +169 -169
  99. package/src/core/PseudoFullscreen.ts +179 -179
  100. package/src/core/ResponsiveManager.ts +231 -231
  101. package/src/core/ResumeManager.ts +236 -236
  102. package/src/core/SignLanguageManager.ts +1308 -1308
  103. package/src/core/ThemeManager.ts +157 -157
  104. package/src/features/PlaylistManager.ts +1838 -1827
  105. package/src/i18n/i18n.ts +223 -223
  106. package/src/i18n/languages/de.ts +244 -217
  107. package/src/i18n/languages/en.ts +245 -218
  108. package/src/i18n/languages/es.ts +241 -214
  109. package/src/i18n/languages/fr.ts +241 -214
  110. package/src/i18n/languages/ja.ts +241 -214
  111. package/src/icons/Icons.ts +168 -167
  112. package/src/index.ts +196 -193
  113. package/src/renderers/DASHRenderer.ts +1077 -1077
  114. package/src/renderers/HLSRenderer.ts +944 -944
  115. package/src/renderers/HTML5Renderer.ts +389 -389
  116. package/src/renderers/SoundCloudRenderer.ts +437 -437
  117. package/src/renderers/VimeoRenderer.ts +298 -298
  118. package/src/renderers/YouTubeRenderer.ts +290 -290
  119. package/src/styles/vidply.css +5852 -5796
  120. package/src/types/events.ts +128 -122
  121. package/src/types/globals.d.ts +313 -313
  122. package/src/types/options.ts +199 -182
  123. package/src/types/renderer.ts +58 -58
  124. package/src/utils/DOMUtils.ts +281 -281
  125. package/src/utils/DownloadInfo.ts +171 -171
  126. package/src/utils/DraggablePanel.ts +539 -539
  127. package/src/utils/DraggablePanelMenu.ts +195 -195
  128. package/src/utils/DraggableResizable.ts +997 -997
  129. package/src/utils/EventEmitter.ts +59 -59
  130. package/src/utils/FormUtils.ts +95 -95
  131. package/src/utils/MenuFactory.ts +483 -483
  132. package/src/utils/Sanitize.ts +70 -70
  133. package/src/utils/SettingsMenuFactory.ts +289 -289
  134. package/src/utils/StorageManager.ts +287 -287
  135. package/src/utils/TrackLabelUtils.ts +52 -52
  136. package/src/utils/UrlSafe.ts +69 -69
  137. package/src/utils/VideoFrameCapture.ts +114 -114
  138. package/src/utils/WindowComponents.ts +250 -250
  139. package/dist/dev/vidply.AudioDescriptionManager-R47Q4YR4.js.map +0 -7
  140. package/dist/dev/vidply.DASHRenderer-PWEMC3BT.js.map +0 -7
  141. package/dist/dev/vidply.FloatingPlayerManager-PKNNNAS2.js.map +0 -7
  142. package/dist/dev/vidply.HLSRenderer-E7RQ74HI.js.map +0 -7
  143. package/dist/dev/vidply.HTML5Renderer-BU4QJJJY.js +0 -12
  144. package/dist/dev/vidply.SignLanguageManager-FET5FQZ7.js.map +0 -7
  145. package/dist/dev/vidply.SoundCloudRenderer-RVSJZODO.js.map +0 -7
  146. package/dist/dev/vidply.TranscriptManager-5GXOQCLX.js.map +0 -7
  147. package/dist/dev/vidply.VimeoRenderer-ER2LSBBW.js.map +0 -7
  148. package/dist/dev/vidply.YouTubeRenderer-NFTOWJAB.js.map +0 -7
  149. package/dist/dev/vidply.chunk-64WXMTGL.js.map +0 -7
  150. package/dist/dev/vidply.chunk-BNFIUL2Z.js.map +0 -7
  151. package/dist/dev/vidply.chunk-JJ47NO24.js.map +0 -7
  152. package/dist/dev/vidply.chunk-KI7C3QVL.js.map +0 -7
  153. package/dist/dev/vidply.chunk-NQPURMQY.js.map +0 -7
  154. package/dist/dev/vidply.chunk-RZWZGDV7.js.map +0 -7
  155. package/dist/dev/vidply.chunk-W25HUCHS.js.map +0 -7
  156. package/dist/dev/vidply.de-4YAT7FFM.js.map +0 -7
  157. package/dist/dev/vidply.es-LEWIRUBH.js.map +0 -7
  158. package/dist/dev/vidply.fr-Q4GV2NOE.js.map +0 -7
  159. package/dist/dev/vidply.ja-S6ITA6WU.js.map +0 -7
  160. package/dist/prod/vidply.AudioDescriptionManager-RHLIJW5C.min.js +0 -6
  161. package/dist/prod/vidply.HTML5Renderer-IU5T3WEW.min.js +0 -6
  162. /package/dist/dev/{vidply.CaptionStyleMenu-7XHRBWUM.js.map → vidply.CaptionStyleMenu-47RCKQF5.js.map} +0 -0
  163. /package/dist/dev/{vidply.HTML5Renderer-BU4QJJJY.js.map → vidply.HTML5Renderer-HLRQUOCU.js.map} +0 -0
  164. /package/dist/dev/{vidply.chunk-SK4V2DAU.js.map → vidply.chunk-HZ7UOKOU.js.map} +0 -0
  165. /package/dist/dev/{vidply.chunk-DZSDO3B7.js.map → vidply.chunk-JZNZX32Z.js.map} +0 -0
  166. /package/dist/dev/{vidply.chunk-VMSQAZM7.js.map → vidply.chunk-P25MRJ6O.js.map} +0 -0
  167. /package/dist/dev/{vidply.chunk-6RQA4ROD.js.map → vidply.chunk-PRFNESYA.js.map} +0 -0
@@ -1,488 +1,488 @@
1
- /**
2
- * Settings Dialog Component
3
- */
4
-
5
- import { DOMUtils } from '../utils/DOMUtils.js';
6
- import { createIconElement } from '../icons/Icons.js';
7
- import { i18n } from '../i18n/i18n.js';
8
- import type { Player } from '../core/Player.js';
9
- import type { CaptionManager } from './CaptionManager.js';
10
-
11
- export class SettingsDialog {
12
- player: Player;
13
- element!: HTMLElement;
14
- isOpen: boolean;
15
- overlay!: HTMLElement;
16
- /**
17
- * Trigger element captured on `show()` so we can return focus to the
18
- * exact button the user activated when the dialog closes.
19
- */
20
- private _triggerElement: HTMLElement | null = null;
21
- private _keydownHandler: ((e: KeyboardEvent) => void) | null = null;
22
-
23
- constructor(player: Player) {
24
- this.player = player;
25
- this.isOpen = false;
26
-
27
- this.init();
28
- }
29
-
30
- init() {
31
- this.createElement();
32
- }
33
-
34
- createElement() {
35
- // Create overlay
36
- // Labelled by the visible <h2> title (created below) rather than a
37
- // duplicate aria-label, so the accessible name matches what is on screen
38
- // (WCAG 2.4.6, 4.1.2).
39
- this.overlay = DOMUtils.createElement('div', {
40
- className: `${this.player.options.classPrefix}-settings-overlay`,
41
- attributes: {
42
- 'role': 'dialog',
43
- 'aria-modal': 'true',
44
- 'aria-labelledby': `${this.player.options.classPrefix}-settings-title`
45
- }
46
- });
47
-
48
- this.overlay.style.display = 'none';
49
-
50
- // Create dialog
51
- this.element = DOMUtils.createElement('div', {
52
- className: `${this.player.options.classPrefix}-settings-dialog`
53
- });
54
-
55
- // Header
56
- const header = DOMUtils.createElement('div', {
57
- className: `${this.player.options.classPrefix}-settings-header`
58
- });
59
-
60
- const title = DOMUtils.createElement('h2', {
61
- textContent: i18n.t('settings.title'),
62
- attributes: {
63
- 'id': `${this.player.options.classPrefix}-settings-title`
64
- }
65
- });
66
-
67
- const closeButton = DOMUtils.createElement('button', {
68
- className: `${this.player.options.classPrefix}-button ${this.player.options.classPrefix}-settings-close`,
69
- attributes: {
70
- 'type': 'button',
71
- 'aria-label': i18n.t('settings.close')
72
- }
73
- });
74
- closeButton.appendChild(createIconElement('close'));
75
- closeButton.addEventListener('click', () => this.hide());
76
-
77
- header.appendChild(title);
78
- header.appendChild(closeButton);
79
-
80
- // Content
81
- const content = DOMUtils.createElement('div', {
82
- className: `${this.player.options.classPrefix}-settings-content`
83
- });
84
-
85
- content.appendChild(this.createSpeedSettings());
86
-
87
- if (this.player.captionManager && this.player.captionManager.tracks.length > 0) {
88
- content.appendChild(this.createCaptionSettings());
89
- }
90
-
91
- // Footer
92
- const footer = DOMUtils.createElement('div', {
93
- className: `${this.player.options.classPrefix}-settings-footer`
94
- });
95
-
96
- const resetButton = DOMUtils.createElement('button', {
97
- className: `${this.player.options.classPrefix}-button`,
98
- textContent: i18n.t('settings.reset'),
99
- attributes: {
100
- 'type': 'button'
101
- }
102
- });
103
- resetButton.addEventListener('click', () => this.resetSettings());
104
-
105
- footer.appendChild(resetButton);
106
-
107
- // Assemble dialog
108
- this.element.appendChild(header);
109
- this.element.appendChild(content);
110
- this.element.appendChild(footer);
111
-
112
- this.overlay.appendChild(this.element);
113
- this.player.container.appendChild(this.overlay);
114
-
115
- // Attach events
116
- this.overlay.addEventListener('click', (e: MouseEvent) => {
117
- if (e.target === this.overlay) {
118
- this.hide();
119
- }
120
- });
121
-
122
- // Escape key to close. The handler is stored so `destroy()` can
123
- // remove it; the player's lifecycle AbortSignal is also forwarded so
124
- // teardown of the player tears down the listener with us.
125
- this._keydownHandler = (e: KeyboardEvent) => {
126
- if (!this.isOpen) return;
127
-
128
- if (e.key === 'Escape') {
129
- this.hide();
130
- return;
131
- }
132
-
133
- // Trap Tab within the modal dialog so keyboard focus cannot reach the
134
- // inert background content behind the overlay (WCAG 2.1.2, 2.4.3).
135
- if (e.key === 'Tab') {
136
- const focusable = Array.from(
137
- this.overlay.querySelectorAll<HTMLElement>(
138
- 'button, [href], select, input, textarea, [tabindex]:not([tabindex="-1"])'
139
- )
140
- ).filter((el) => !el.hasAttribute('disabled') && el.offsetParent !== null);
141
- if (focusable.length === 0) return;
142
-
143
- const first = focusable[0];
144
- const last = focusable[focusable.length - 1];
145
- if (!first || !last) return;
146
- const active = document.activeElement as HTMLElement | null;
147
-
148
- if (e.shiftKey && (active === first || !this.overlay.contains(active))) {
149
- e.preventDefault();
150
- last.focus({ preventScroll: true });
151
- } else if (!e.shiftKey && active === last) {
152
- e.preventDefault();
153
- first.focus({ preventScroll: true });
154
- }
155
- }
156
- };
157
- const lifecycleSignal = (this.player as { lifecycleSignal?: AbortSignal }).lifecycleSignal;
158
- document.addEventListener('keydown', this._keydownHandler, lifecycleSignal ? { signal: lifecycleSignal } : undefined);
159
- }
160
-
161
- formatSpeedLabel(speed: number) {
162
- // Special case: 1x is "Normal" (translated)
163
- if (speed === 1) {
164
- return i18n.t('speeds.normal');
165
- }
166
-
167
- // For other speeds, format with locale-specific decimal separator
168
- const speedStr = speed.toLocaleString(i18n.getLanguage(), {
169
- minimumFractionDigits: 0,
170
- maximumFractionDigits: 2
171
- });
172
-
173
- return `${speedStr}×`;
174
- }
175
-
176
- createSpeedSettings() {
177
- const section = DOMUtils.createElement('div', {
178
- className: `${this.player.options.classPrefix}-settings-section`
179
- });
180
-
181
- const label = DOMUtils.createElement('label', {
182
- textContent: i18n.t('settings.speed'),
183
- attributes: {
184
- 'for': `${this.player.options.classPrefix}-speed-select`
185
- }
186
- });
187
-
188
- const select = DOMUtils.createElement('select', {
189
- className: `${this.player.options.classPrefix}-settings-select`,
190
- attributes: {
191
- 'id': `${this.player.options.classPrefix}-speed-select`
192
- }
193
- });
194
-
195
- const speeds = [0.25, 0.5, 0.75, 1, 1.25, 1.5, 1.75, 2];
196
-
197
- speeds.forEach((speed: number) => {
198
- const option = DOMUtils.createElement('option', {
199
- textContent: this.formatSpeedLabel(speed),
200
- attributes: {
201
- 'value': String(speed)
202
- }
203
- });
204
-
205
- if (speed === this.player.state.playbackSpeed) {
206
- (option as HTMLOptionElement).selected = true;
207
- }
208
-
209
- select.appendChild(option);
210
- });
211
-
212
- select.addEventListener('change', (e) => {
213
- this.player.setPlaybackSpeed(parseFloat((e.target as HTMLSelectElement).value));
214
- });
215
-
216
- section.appendChild(label);
217
- section.appendChild(select);
218
-
219
- return section;
220
- }
221
-
222
- createCaptionSettings() {
223
- const section = DOMUtils.createElement('div', {
224
- className: `${this.player.options.classPrefix}-settings-section`
225
- });
226
-
227
- const heading = DOMUtils.createElement('h3', {
228
- textContent: i18n.t('settings.captions')
229
- });
230
-
231
- section.appendChild(heading);
232
-
233
- // Caption track selection
234
- const trackLabel = DOMUtils.createElement('label', {
235
- textContent: i18n.t('captions.select'),
236
- attributes: {
237
- 'for': `${this.player.options.classPrefix}-caption-track-select`
238
- }
239
- });
240
-
241
- const trackSelect = DOMUtils.createElement('select', {
242
- className: `${this.player.options.classPrefix}-settings-select`,
243
- attributes: {
244
- 'id': `${this.player.options.classPrefix}-caption-track-select`
245
- }
246
- });
247
-
248
- // Off option
249
- const offOption = DOMUtils.createElement('option', {
250
- textContent: i18n.t('captions.off'),
251
- attributes: { 'value': '-1' }
252
- });
253
- trackSelect.appendChild(offOption);
254
-
255
- const tracks: ReturnType<CaptionManager['getAvailableTracks']> =
256
- this.player.captionManager?.getAvailableTracks() ?? [];
257
- tracks.forEach((track) => {
258
- const attributes: Record<string, string> = { 'value': String(track.index) };
259
- if (track.language) {
260
- attributes['lang'] = track.language;
261
- }
262
- const option = DOMUtils.createElement('option', {
263
- textContent: track.label,
264
- attributes: attributes
265
- });
266
- trackSelect.appendChild(option);
267
- });
268
-
269
- trackSelect.addEventListener('change', (e) => {
270
- const index = parseInt((e.target as HTMLSelectElement).value);
271
- if (index === -1) {
272
- this.player.disableCaptions();
273
- } else {
274
- this.player.captionManager?.switchTrack(index);
275
- }
276
- });
277
-
278
- section.appendChild(trackLabel);
279
- section.appendChild(trackSelect);
280
-
281
- // Font size
282
- section.appendChild(this.createCaptionStyleControl('fontSize', i18n.t('captions.fontSize'), [
283
- { label: i18n.t('fontSizes.small'), value: '87.5%' },
284
- { label: i18n.t('fontSizes.normal'), value: '100%' },
285
- { label: i18n.t('fontSizes.large'), value: '125%' },
286
- { label: i18n.t('fontSizes.xlarge'), value: '150%' }
287
- ]));
288
-
289
- // Font family
290
- section.appendChild(this.createCaptionStyleControl('fontFamily', i18n.t('captions.fontFamily'), [
291
- { label: i18n.t('fontFamilies.sansSerif'), value: 'sans-serif' },
292
- { label: i18n.t('fontFamilies.serif'), value: 'serif' },
293
- { label: i18n.t('fontFamilies.monospace'), value: 'monospace' }
294
- ]));
295
-
296
- // Color controls
297
- section.appendChild(this.createColorControl('color', i18n.t('captions.color')));
298
- section.appendChild(this.createColorControl('backgroundColor', i18n.t('captions.backgroundColor')));
299
-
300
- // Opacity
301
- section.appendChild(this.createRangeControl('opacity', i18n.t('captions.opacity'), 0, 1, 0.1));
302
-
303
- return section;
304
- }
305
-
306
- createCaptionStyleControl(property: string, label: string, options: Array<{ label: string; value: string }>) {
307
- const wrapper = DOMUtils.createElement('div', {
308
- className: `${this.player.options.classPrefix}-settings-control`
309
- });
310
-
311
- const labelEl = DOMUtils.createElement('label', {
312
- textContent: label,
313
- attributes: {
314
- 'for': `${this.player.options.classPrefix}-caption-${property}`
315
- }
316
- });
317
-
318
- const select = DOMUtils.createElement('select', {
319
- className: `${this.player.options.classPrefix}-settings-select`,
320
- attributes: {
321
- 'id': `${this.player.options.classPrefix}-caption-${property}`
322
- }
323
- });
324
-
325
- options.forEach((opt: { label: string; value: string }) => {
326
- const option = DOMUtils.createElement('option', {
327
- textContent: opt.label,
328
- attributes: { 'value': opt.value }
329
- });
330
-
331
- if (opt.value === this.player.options[`captions${property.charAt(0).toUpperCase() + property.slice(1)}`]) {
332
- (option as HTMLOptionElement).selected = true;
333
- }
334
-
335
- select.appendChild(option);
336
- });
337
-
338
- select.addEventListener('change', (e) => {
339
- this.player.captionManager?.setCaptionStyle(property, (e.target as HTMLSelectElement).value);
340
- });
341
-
342
- wrapper.appendChild(labelEl);
343
- wrapper.appendChild(select);
344
-
345
- return wrapper;
346
- }
347
-
348
- createColorControl(property: string, label: string) {
349
- const wrapper = DOMUtils.createElement('div', {
350
- className: `${this.player.options.classPrefix}-settings-control`
351
- });
352
-
353
- const labelEl = DOMUtils.createElement('label', {
354
- textContent: label,
355
- attributes: {
356
- 'for': `${this.player.options.classPrefix}-caption-${property}`
357
- }
358
- });
359
-
360
- const input = DOMUtils.createElement('input', {
361
- className: `${this.player.options.classPrefix}-settings-color`,
362
- attributes: {
363
- 'type': 'color',
364
- 'id': `${this.player.options.classPrefix}-caption-${property}`,
365
- 'value': String(this.player.options[`captions${property.charAt(0).toUpperCase() + property.slice(1)}`] ?? '')
366
- }
367
- });
368
-
369
- input.addEventListener('change', (e) => {
370
- this.player.captionManager?.setCaptionStyle(property, (e.target as HTMLInputElement).value);
371
- });
372
-
373
- wrapper.appendChild(labelEl);
374
- wrapper.appendChild(input);
375
-
376
- return wrapper;
377
- }
378
-
379
- createRangeControl(property: string, label: string, min: number, max: number, step: number) {
380
- const wrapper = DOMUtils.createElement('div', {
381
- className: `${this.player.options.classPrefix}-settings-control`
382
- });
383
-
384
- const labelEl = DOMUtils.createElement('label', {
385
- textContent: label,
386
- attributes: {
387
- 'for': `${this.player.options.classPrefix}-caption-${property}`
388
- }
389
- });
390
-
391
- const input = DOMUtils.createElement('input', {
392
- className: `${this.player.options.classPrefix}-settings-range`,
393
- attributes: {
394
- 'type': 'range',
395
- 'id': `${this.player.options.classPrefix}-caption-${property}`,
396
- 'min': String(min),
397
- 'max': String(max),
398
- 'step': String(step),
399
- 'value': String(this.player.options[`captions${property.charAt(0).toUpperCase() + property.slice(1)}`])
400
- }
401
- });
402
-
403
- const valueDisplay = DOMUtils.createElement('span', {
404
- className: `${this.player.options.classPrefix}-settings-value`,
405
- textContent: String(this.player.options[`captions${property.charAt(0).toUpperCase() + property.slice(1)}`])
406
- });
407
-
408
- input.addEventListener('input', (e) => {
409
- const value = parseFloat((e.target as HTMLInputElement).value);
410
- valueDisplay.textContent = value.toFixed(1);
411
- this.player.captionManager?.setCaptionStyle(property, value);
412
- });
413
-
414
- wrapper.appendChild(labelEl);
415
- wrapper.appendChild(input);
416
- wrapper.appendChild(valueDisplay);
417
-
418
- return wrapper;
419
- }
420
-
421
- resetSettings() {
422
- // Reset to default values
423
- this.player.setPlaybackSpeed(1);
424
-
425
- if (this.player.captionManager) {
426
- this.player.captionManager.setCaptionStyle('fontSize', '100%');
427
- this.player.captionManager.setCaptionStyle('fontFamily', 'sans-serif');
428
- this.player.captionManager.setCaptionStyle('color', '#FFFFFF');
429
- this.player.captionManager.setCaptionStyle('backgroundColor', '#000000');
430
- this.player.captionManager.setCaptionStyle('opacity', 0.8);
431
- }
432
-
433
- // Refresh dialog
434
- this.hide();
435
- setTimeout(() => this.show(), 100);
436
- }
437
-
438
- show() {
439
- // Capture the element that opened the dialog so focus can be restored
440
- // to it when the dialog closes.
441
- const active = (typeof document !== 'undefined' ? document.activeElement : null) as HTMLElement | null;
442
- this._triggerElement = active && typeof active.focus === 'function' ? active : null;
443
-
444
- this.overlay.style.display = 'flex';
445
- this.isOpen = true;
446
-
447
- const closeButton = this.element.querySelector<HTMLElement>(`.${this.player.options.classPrefix}-settings-close`);
448
- if (closeButton) {
449
- closeButton.focus();
450
- }
451
-
452
- this.player.emit('settingsopen');
453
- }
454
-
455
- hide() {
456
- this.overlay.style.display = 'none';
457
- this.isOpen = false;
458
-
459
- // Return focus to whichever element opened the dialog. Falls back to
460
- // the player container if the trigger was removed from the DOM in the
461
- // meantime.
462
- const trigger = this._triggerElement;
463
- this._triggerElement = null;
464
- if (trigger && document.contains(trigger)) {
465
- try {
466
- trigger.focus({ preventScroll: true });
467
- } catch {
468
- this.player.container.focus();
469
- }
470
- } else if (this.player.container) {
471
- this.player.container.focus();
472
- }
473
-
474
- this.player.emit('settingsclose');
475
- }
476
-
477
- destroy() {
478
- if (this._keydownHandler) {
479
- document.removeEventListener('keydown', this._keydownHandler);
480
- this._keydownHandler = null;
481
- }
482
- if (this.overlay && this.overlay.parentNode) {
483
- this.overlay.parentNode.removeChild(this.overlay);
484
- }
485
- this._triggerElement = null;
486
- }
487
- }
488
-
1
+ /**
2
+ * Settings Dialog Component
3
+ */
4
+
5
+ import { DOMUtils } from '../utils/DOMUtils.js';
6
+ import { createIconElement } from '../icons/Icons.js';
7
+ import { i18n } from '../i18n/i18n.js';
8
+ import type { Player } from '../core/Player.js';
9
+ import type { CaptionManager } from './CaptionManager.js';
10
+
11
+ export class SettingsDialog {
12
+ player: Player;
13
+ element!: HTMLElement;
14
+ isOpen: boolean;
15
+ overlay!: HTMLElement;
16
+ /**
17
+ * Trigger element captured on `show()` so we can return focus to the
18
+ * exact button the user activated when the dialog closes.
19
+ */
20
+ private _triggerElement: HTMLElement | null = null;
21
+ private _keydownHandler: ((e: KeyboardEvent) => void) | null = null;
22
+
23
+ constructor(player: Player) {
24
+ this.player = player;
25
+ this.isOpen = false;
26
+
27
+ this.init();
28
+ }
29
+
30
+ init() {
31
+ this.createElement();
32
+ }
33
+
34
+ createElement() {
35
+ // Create overlay
36
+ // Labelled by the visible <h2> title (created below) rather than a
37
+ // duplicate aria-label, so the accessible name matches what is on screen
38
+ // (WCAG 2.4.6, 4.1.2).
39
+ this.overlay = DOMUtils.createElement('div', {
40
+ className: `${this.player.options.classPrefix}-settings-overlay`,
41
+ attributes: {
42
+ 'role': 'dialog',
43
+ 'aria-modal': 'true',
44
+ 'aria-labelledby': `${this.player.options.classPrefix}-settings-title`
45
+ }
46
+ });
47
+
48
+ this.overlay.style.display = 'none';
49
+
50
+ // Create dialog
51
+ this.element = DOMUtils.createElement('div', {
52
+ className: `${this.player.options.classPrefix}-settings-dialog`
53
+ });
54
+
55
+ // Header
56
+ const header = DOMUtils.createElement('div', {
57
+ className: `${this.player.options.classPrefix}-settings-header`
58
+ });
59
+
60
+ const title = DOMUtils.createElement('h2', {
61
+ textContent: i18n.t('settings.title'),
62
+ attributes: {
63
+ 'id': `${this.player.options.classPrefix}-settings-title`
64
+ }
65
+ });
66
+
67
+ const closeButton = DOMUtils.createElement('button', {
68
+ className: `${this.player.options.classPrefix}-button ${this.player.options.classPrefix}-settings-close`,
69
+ attributes: {
70
+ 'type': 'button',
71
+ 'aria-label': i18n.t('settings.close')
72
+ }
73
+ });
74
+ closeButton.appendChild(createIconElement('close'));
75
+ closeButton.addEventListener('click', () => this.hide());
76
+
77
+ header.appendChild(title);
78
+ header.appendChild(closeButton);
79
+
80
+ // Content
81
+ const content = DOMUtils.createElement('div', {
82
+ className: `${this.player.options.classPrefix}-settings-content`
83
+ });
84
+
85
+ content.appendChild(this.createSpeedSettings());
86
+
87
+ if (this.player.captionManager && this.player.captionManager.tracks.length > 0) {
88
+ content.appendChild(this.createCaptionSettings());
89
+ }
90
+
91
+ // Footer
92
+ const footer = DOMUtils.createElement('div', {
93
+ className: `${this.player.options.classPrefix}-settings-footer`
94
+ });
95
+
96
+ const resetButton = DOMUtils.createElement('button', {
97
+ className: `${this.player.options.classPrefix}-button`,
98
+ textContent: i18n.t('settings.reset'),
99
+ attributes: {
100
+ 'type': 'button'
101
+ }
102
+ });
103
+ resetButton.addEventListener('click', () => this.resetSettings());
104
+
105
+ footer.appendChild(resetButton);
106
+
107
+ // Assemble dialog
108
+ this.element.appendChild(header);
109
+ this.element.appendChild(content);
110
+ this.element.appendChild(footer);
111
+
112
+ this.overlay.appendChild(this.element);
113
+ this.player.container.appendChild(this.overlay);
114
+
115
+ // Attach events
116
+ this.overlay.addEventListener('click', (e: MouseEvent) => {
117
+ if (e.target === this.overlay) {
118
+ this.hide();
119
+ }
120
+ });
121
+
122
+ // Escape key to close. The handler is stored so `destroy()` can
123
+ // remove it; the player's lifecycle AbortSignal is also forwarded so
124
+ // teardown of the player tears down the listener with us.
125
+ this._keydownHandler = (e: KeyboardEvent) => {
126
+ if (!this.isOpen) return;
127
+
128
+ if (e.key === 'Escape') {
129
+ this.hide();
130
+ return;
131
+ }
132
+
133
+ // Trap Tab within the modal dialog so keyboard focus cannot reach the
134
+ // inert background content behind the overlay (WCAG 2.1.2, 2.4.3).
135
+ if (e.key === 'Tab') {
136
+ const focusable = Array.from(
137
+ this.overlay.querySelectorAll<HTMLElement>(
138
+ 'button, [href], select, input, textarea, [tabindex]:not([tabindex="-1"])'
139
+ )
140
+ ).filter((el) => !el.hasAttribute('disabled') && el.offsetParent !== null);
141
+ if (focusable.length === 0) return;
142
+
143
+ const first = focusable[0];
144
+ const last = focusable[focusable.length - 1];
145
+ if (!first || !last) return;
146
+ const active = document.activeElement as HTMLElement | null;
147
+
148
+ if (e.shiftKey && (active === first || !this.overlay.contains(active))) {
149
+ e.preventDefault();
150
+ last.focus({ preventScroll: true });
151
+ } else if (!e.shiftKey && active === last) {
152
+ e.preventDefault();
153
+ first.focus({ preventScroll: true });
154
+ }
155
+ }
156
+ };
157
+ const lifecycleSignal = (this.player as { lifecycleSignal?: AbortSignal }).lifecycleSignal;
158
+ document.addEventListener('keydown', this._keydownHandler, lifecycleSignal ? { signal: lifecycleSignal } : undefined);
159
+ }
160
+
161
+ formatSpeedLabel(speed: number) {
162
+ // Special case: 1x is "Normal" (translated)
163
+ if (speed === 1) {
164
+ return i18n.t('speeds.normal');
165
+ }
166
+
167
+ // For other speeds, format with locale-specific decimal separator
168
+ const speedStr = speed.toLocaleString(i18n.getLanguage(), {
169
+ minimumFractionDigits: 0,
170
+ maximumFractionDigits: 2
171
+ });
172
+
173
+ return `${speedStr}×`;
174
+ }
175
+
176
+ createSpeedSettings() {
177
+ const section = DOMUtils.createElement('div', {
178
+ className: `${this.player.options.classPrefix}-settings-section`
179
+ });
180
+
181
+ const label = DOMUtils.createElement('label', {
182
+ textContent: i18n.t('settings.speed'),
183
+ attributes: {
184
+ 'for': `${this.player.options.classPrefix}-speed-select`
185
+ }
186
+ });
187
+
188
+ const select = DOMUtils.createElement('select', {
189
+ className: `${this.player.options.classPrefix}-settings-select`,
190
+ attributes: {
191
+ 'id': `${this.player.options.classPrefix}-speed-select`
192
+ }
193
+ });
194
+
195
+ const speeds = [0.25, 0.5, 0.75, 1, 1.25, 1.5, 1.75, 2];
196
+
197
+ speeds.forEach((speed: number) => {
198
+ const option = DOMUtils.createElement('option', {
199
+ textContent: this.formatSpeedLabel(speed),
200
+ attributes: {
201
+ 'value': String(speed)
202
+ }
203
+ });
204
+
205
+ if (speed === this.player.state.playbackSpeed) {
206
+ (option as HTMLOptionElement).selected = true;
207
+ }
208
+
209
+ select.appendChild(option);
210
+ });
211
+
212
+ select.addEventListener('change', (e) => {
213
+ this.player.setPlaybackSpeed(parseFloat((e.target as HTMLSelectElement).value));
214
+ });
215
+
216
+ section.appendChild(label);
217
+ section.appendChild(select);
218
+
219
+ return section;
220
+ }
221
+
222
+ createCaptionSettings() {
223
+ const section = DOMUtils.createElement('div', {
224
+ className: `${this.player.options.classPrefix}-settings-section`
225
+ });
226
+
227
+ const heading = DOMUtils.createElement('h3', {
228
+ textContent: i18n.t('settings.captions')
229
+ });
230
+
231
+ section.appendChild(heading);
232
+
233
+ // Caption track selection
234
+ const trackLabel = DOMUtils.createElement('label', {
235
+ textContent: i18n.t('captions.select'),
236
+ attributes: {
237
+ 'for': `${this.player.options.classPrefix}-caption-track-select`
238
+ }
239
+ });
240
+
241
+ const trackSelect = DOMUtils.createElement('select', {
242
+ className: `${this.player.options.classPrefix}-settings-select`,
243
+ attributes: {
244
+ 'id': `${this.player.options.classPrefix}-caption-track-select`
245
+ }
246
+ });
247
+
248
+ // Off option
249
+ const offOption = DOMUtils.createElement('option', {
250
+ textContent: i18n.t('captions.off'),
251
+ attributes: { 'value': '-1' }
252
+ });
253
+ trackSelect.appendChild(offOption);
254
+
255
+ const tracks: ReturnType<CaptionManager['getAvailableTracks']> =
256
+ this.player.captionManager?.getAvailableTracks() ?? [];
257
+ tracks.forEach((track) => {
258
+ const attributes: Record<string, string> = { 'value': String(track.index) };
259
+ if (track.language) {
260
+ attributes['lang'] = track.language;
261
+ }
262
+ const option = DOMUtils.createElement('option', {
263
+ textContent: track.label,
264
+ attributes: attributes
265
+ });
266
+ trackSelect.appendChild(option);
267
+ });
268
+
269
+ trackSelect.addEventListener('change', (e) => {
270
+ const index = parseInt((e.target as HTMLSelectElement).value);
271
+ if (index === -1) {
272
+ this.player.disableCaptions();
273
+ } else {
274
+ this.player.captionManager?.switchTrack(index);
275
+ }
276
+ });
277
+
278
+ section.appendChild(trackLabel);
279
+ section.appendChild(trackSelect);
280
+
281
+ // Font size
282
+ section.appendChild(this.createCaptionStyleControl('fontSize', i18n.t('captions.fontSize'), [
283
+ { label: i18n.t('fontSizes.small'), value: '87.5%' },
284
+ { label: i18n.t('fontSizes.normal'), value: '100%' },
285
+ { label: i18n.t('fontSizes.large'), value: '125%' },
286
+ { label: i18n.t('fontSizes.xlarge'), value: '150%' }
287
+ ]));
288
+
289
+ // Font family
290
+ section.appendChild(this.createCaptionStyleControl('fontFamily', i18n.t('captions.fontFamily'), [
291
+ { label: i18n.t('fontFamilies.sansSerif'), value: 'sans-serif' },
292
+ { label: i18n.t('fontFamilies.serif'), value: 'serif' },
293
+ { label: i18n.t('fontFamilies.monospace'), value: 'monospace' }
294
+ ]));
295
+
296
+ // Color controls
297
+ section.appendChild(this.createColorControl('color', i18n.t('captions.color')));
298
+ section.appendChild(this.createColorControl('backgroundColor', i18n.t('captions.backgroundColor')));
299
+
300
+ // Opacity
301
+ section.appendChild(this.createRangeControl('opacity', i18n.t('captions.opacity'), 0, 1, 0.1));
302
+
303
+ return section;
304
+ }
305
+
306
+ createCaptionStyleControl(property: string, label: string, options: Array<{ label: string; value: string }>) {
307
+ const wrapper = DOMUtils.createElement('div', {
308
+ className: `${this.player.options.classPrefix}-settings-control`
309
+ });
310
+
311
+ const labelEl = DOMUtils.createElement('label', {
312
+ textContent: label,
313
+ attributes: {
314
+ 'for': `${this.player.options.classPrefix}-caption-${property}`
315
+ }
316
+ });
317
+
318
+ const select = DOMUtils.createElement('select', {
319
+ className: `${this.player.options.classPrefix}-settings-select`,
320
+ attributes: {
321
+ 'id': `${this.player.options.classPrefix}-caption-${property}`
322
+ }
323
+ });
324
+
325
+ options.forEach((opt: { label: string; value: string }) => {
326
+ const option = DOMUtils.createElement('option', {
327
+ textContent: opt.label,
328
+ attributes: { 'value': opt.value }
329
+ });
330
+
331
+ if (opt.value === this.player.options[`captions${property.charAt(0).toUpperCase() + property.slice(1)}`]) {
332
+ (option as HTMLOptionElement).selected = true;
333
+ }
334
+
335
+ select.appendChild(option);
336
+ });
337
+
338
+ select.addEventListener('change', (e) => {
339
+ this.player.captionManager?.setCaptionStyle(property, (e.target as HTMLSelectElement).value);
340
+ });
341
+
342
+ wrapper.appendChild(labelEl);
343
+ wrapper.appendChild(select);
344
+
345
+ return wrapper;
346
+ }
347
+
348
+ createColorControl(property: string, label: string) {
349
+ const wrapper = DOMUtils.createElement('div', {
350
+ className: `${this.player.options.classPrefix}-settings-control`
351
+ });
352
+
353
+ const labelEl = DOMUtils.createElement('label', {
354
+ textContent: label,
355
+ attributes: {
356
+ 'for': `${this.player.options.classPrefix}-caption-${property}`
357
+ }
358
+ });
359
+
360
+ const input = DOMUtils.createElement('input', {
361
+ className: `${this.player.options.classPrefix}-settings-color`,
362
+ attributes: {
363
+ 'type': 'color',
364
+ 'id': `${this.player.options.classPrefix}-caption-${property}`,
365
+ 'value': String(this.player.options[`captions${property.charAt(0).toUpperCase() + property.slice(1)}`] ?? '')
366
+ }
367
+ });
368
+
369
+ input.addEventListener('change', (e) => {
370
+ this.player.captionManager?.setCaptionStyle(property, (e.target as HTMLInputElement).value);
371
+ });
372
+
373
+ wrapper.appendChild(labelEl);
374
+ wrapper.appendChild(input);
375
+
376
+ return wrapper;
377
+ }
378
+
379
+ createRangeControl(property: string, label: string, min: number, max: number, step: number) {
380
+ const wrapper = DOMUtils.createElement('div', {
381
+ className: `${this.player.options.classPrefix}-settings-control`
382
+ });
383
+
384
+ const labelEl = DOMUtils.createElement('label', {
385
+ textContent: label,
386
+ attributes: {
387
+ 'for': `${this.player.options.classPrefix}-caption-${property}`
388
+ }
389
+ });
390
+
391
+ const input = DOMUtils.createElement('input', {
392
+ className: `${this.player.options.classPrefix}-settings-range`,
393
+ attributes: {
394
+ 'type': 'range',
395
+ 'id': `${this.player.options.classPrefix}-caption-${property}`,
396
+ 'min': String(min),
397
+ 'max': String(max),
398
+ 'step': String(step),
399
+ 'value': String(this.player.options[`captions${property.charAt(0).toUpperCase() + property.slice(1)}`])
400
+ }
401
+ });
402
+
403
+ const valueDisplay = DOMUtils.createElement('span', {
404
+ className: `${this.player.options.classPrefix}-settings-value`,
405
+ textContent: String(this.player.options[`captions${property.charAt(0).toUpperCase() + property.slice(1)}`])
406
+ });
407
+
408
+ input.addEventListener('input', (e) => {
409
+ const value = parseFloat((e.target as HTMLInputElement).value);
410
+ valueDisplay.textContent = value.toFixed(1);
411
+ this.player.captionManager?.setCaptionStyle(property, value);
412
+ });
413
+
414
+ wrapper.appendChild(labelEl);
415
+ wrapper.appendChild(input);
416
+ wrapper.appendChild(valueDisplay);
417
+
418
+ return wrapper;
419
+ }
420
+
421
+ resetSettings() {
422
+ // Reset to default values
423
+ this.player.setPlaybackSpeed(1);
424
+
425
+ if (this.player.captionManager) {
426
+ this.player.captionManager.setCaptionStyle('fontSize', '100%');
427
+ this.player.captionManager.setCaptionStyle('fontFamily', 'sans-serif');
428
+ this.player.captionManager.setCaptionStyle('color', '#FFFFFF');
429
+ this.player.captionManager.setCaptionStyle('backgroundColor', '#000000');
430
+ this.player.captionManager.setCaptionStyle('opacity', 0.8);
431
+ }
432
+
433
+ // Refresh dialog
434
+ this.hide();
435
+ setTimeout(() => this.show(), 100);
436
+ }
437
+
438
+ show() {
439
+ // Capture the element that opened the dialog so focus can be restored
440
+ // to it when the dialog closes.
441
+ const active = (typeof document !== 'undefined' ? document.activeElement : null) as HTMLElement | null;
442
+ this._triggerElement = active && typeof active.focus === 'function' ? active : null;
443
+
444
+ this.overlay.style.display = 'flex';
445
+ this.isOpen = true;
446
+
447
+ const closeButton = this.element.querySelector<HTMLElement>(`.${this.player.options.classPrefix}-settings-close`);
448
+ if (closeButton) {
449
+ closeButton.focus();
450
+ }
451
+
452
+ this.player.emit('settingsopen');
453
+ }
454
+
455
+ hide() {
456
+ this.overlay.style.display = 'none';
457
+ this.isOpen = false;
458
+
459
+ // Return focus to whichever element opened the dialog. Falls back to
460
+ // the player container if the trigger was removed from the DOM in the
461
+ // meantime.
462
+ const trigger = this._triggerElement;
463
+ this._triggerElement = null;
464
+ if (trigger && document.contains(trigger)) {
465
+ try {
466
+ trigger.focus({ preventScroll: true });
467
+ } catch {
468
+ this.player.container.focus();
469
+ }
470
+ } else if (this.player.container) {
471
+ this.player.container.focus();
472
+ }
473
+
474
+ this.player.emit('settingsclose');
475
+ }
476
+
477
+ destroy() {
478
+ if (this._keydownHandler) {
479
+ document.removeEventListener('keydown', this._keydownHandler);
480
+ this._keydownHandler = null;
481
+ }
482
+ if (this.overlay && this.overlay.parentNode) {
483
+ this.overlay.parentNode.removeChild(this.overlay);
484
+ }
485
+ this._triggerElement = null;
486
+ }
487
+ }
488
+