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,289 +1,289 @@
1
- /**
2
- * Settings Menu Factory
3
- * Creates standardized popup settings menus with consistent styling
4
- */
5
-
6
- import { DOMUtils } from './DOMUtils.js';
7
- import { createIconElement } from '../icons/Icons.js';
8
- import { i18n } from '../i18n/i18n.js';
9
- import { reducedMotionScrollOptions } from './PerformanceUtils.js';
10
-
11
- interface SettingsMenuItemConfig {
12
- icon?: string;
13
- label: string;
14
- onClick?: ((e: Event) => void) | null;
15
- closeMenuOnClick?: boolean;
16
- }
17
-
18
- interface CreateSettingsMenuOptions {
19
- classPrefix: string;
20
- menuClass: string;
21
- menuId: string;
22
- items?: SettingsMenuItemConfig[];
23
- }
24
-
25
- interface CreateSettingsMenuItemOptions {
26
- classPrefix: string;
27
- itemClass: string;
28
- icon?: string;
29
- label: string;
30
- onClick?: ((e: Event) => void) | null;
31
- closeMenuOnClick?: boolean;
32
- }
33
-
34
- /**
35
- * Create a popup settings menu
36
- * @param {Object} options - Menu options
37
- * @param {string} options.classPrefix - Class prefix
38
- * @param {string} options.menuClass - Specific menu class name
39
- * @param {string} options.menuId - Menu element ID
40
- * @param {Array} options.items - Array of menu item definitions
41
- * @returns {HTMLElement} Menu element
42
- */
43
- export function createSettingsMenu({
44
- classPrefix,
45
- menuClass,
46
- menuId,
47
- items = []
48
- }: CreateSettingsMenuOptions): HTMLElement {
49
- const menu = DOMUtils.createElement('div', {
50
- className: `${classPrefix}-popup-settings-menu ${menuClass}`,
51
- attributes: {
52
- 'id': menuId,
53
- 'role': 'menu',
54
- 'aria-label': i18n.t('settings.title')
55
- }
56
- });
57
-
58
- // Add menu items
59
- items.forEach((item: SettingsMenuItemConfig) => {
60
- const menuItem = createSettingsMenuItem({
61
- classPrefix,
62
- itemClass: `${menuClass}-item`,
63
- ...item
64
- });
65
- menu.appendChild(menuItem);
66
- });
67
-
68
- return menu;
69
- }
70
-
71
- /**
72
- * Create a settings menu item
73
- * @param {Object} options - Item options
74
- * @param {string} options.classPrefix - Class prefix
75
- * @param {string} options.itemClass - Item class name
76
- * @param {string} options.icon - Icon name
77
- * @param {string} options.label - Item label (i18n key or text)
78
- * @param {Function} options.onClick - Click handler
79
- * @param {boolean} options.closeMenuOnClick - Whether to close menu after click
80
- * @returns {HTMLElement} Menu item button
81
- */
82
- export function createSettingsMenuItem({
83
- classPrefix,
84
- itemClass,
85
- icon,
86
- label,
87
- onClick = null,
88
- closeMenuOnClick: _closeMenuOnClick = true
89
- }: CreateSettingsMenuItemOptions): HTMLElement {
90
- // Check if label is an i18n key
91
- const isI18nKey = typeof label === 'string' &&
92
- (label.startsWith('transcript.') ||
93
- label.startsWith('player.') ||
94
- label.startsWith('settings.') ||
95
- label.startsWith('captions.'));
96
- const labelText = isI18nKey ? (i18n.t(label) || label) : label;
97
-
98
- const button = DOMUtils.createElement('button', {
99
- className: `${classPrefix}-popup-settings-item ${itemClass}`,
100
- attributes: {
101
- 'type': 'button',
102
- 'role': 'menuitem',
103
- 'tabindex': '-1'
104
- }
105
- });
106
-
107
- if (icon) {
108
- button.appendChild(createIconElement(icon));
109
- }
110
-
111
- const text = DOMUtils.createElement('span', {
112
- textContent: labelText
113
- });
114
- button.appendChild(text);
115
-
116
- if (onClick) {
117
- button.addEventListener('click', (e) => {
118
- onClick(e);
119
- // Menu closing is handled by the caller if needed
120
- });
121
- }
122
-
123
- return button;
124
- }
125
-
126
- /**
127
- * Show a popup settings menu
128
- * @param {HTMLElement} menu - Menu element
129
- * @param {HTMLElement} button - Trigger button
130
- * @param {string} classPrefix - Class prefix
131
- */
132
- export function showSettingsMenu(menu: HTMLElement, button: HTMLElement, classPrefix: string): void {
133
- if (!menu || !button) return;
134
-
135
- // Position menu relative to button
136
- menu.style.display = 'block';
137
-
138
- // Check if menu would go off-screen
139
- const menuRect = menu.getBoundingClientRect();
140
- const buttonRect = button.getBoundingClientRect();
141
- const viewportHeight = window.innerHeight;
142
-
143
- // Position above or below based on space
144
- if (buttonRect.top - menuRect.height < 0 && buttonRect.bottom + menuRect.height < viewportHeight) {
145
- // Position below
146
- menu.style.top = '100%';
147
- menu.style.bottom = 'auto';
148
- DOMUtils.removeClass(menu, `${classPrefix}-menu-above`);
149
- } else {
150
- // Position above
151
- menu.style.bottom = '100%';
152
- menu.style.top = 'auto';
153
- DOMUtils.addClass(menu, `${classPrefix}-menu-above`);
154
- }
155
-
156
- // Focus first item
157
- const firstItem = menu.querySelector('[role="menuitem"]') as HTMLElement | null;
158
- if (firstItem) {
159
- firstItem.setAttribute('tabindex', '0');
160
- setTimeout(() => firstItem.focus(), 50);
161
- }
162
- }
163
-
164
- /**
165
- * Hide a popup settings menu
166
- * @param {HTMLElement} menu - Menu element
167
- * @param {HTMLElement} button - Trigger button (to return focus)
168
- */
169
- export function hideSettingsMenu(menu: HTMLElement, button: HTMLElement | null): void {
170
- if (!menu) return;
171
-
172
- menu.style.display = 'none';
173
-
174
- // Return focus to trigger button
175
- if (button && document.contains(button)) {
176
- button.focus();
177
- }
178
- }
179
-
180
- /**
181
- * Toggle a popup settings menu
182
- * @param {HTMLElement} menu - Menu element
183
- * @param {HTMLElement} button - Trigger button
184
- * @param {string} classPrefix - Class prefix
185
- */
186
- export function toggleSettingsMenu(menu: HTMLElement, button: HTMLElement | null, classPrefix: string): void {
187
- if (!menu || !button) return;
188
-
189
- if (menu.style.display === 'block') {
190
- hideSettingsMenu(menu, button);
191
- } else {
192
- showSettingsMenu(menu, button, classPrefix);
193
- }
194
- }
195
-
196
- /**
197
- * Setup keyboard navigation for settings menu
198
- * @param {HTMLElement} menu - Menu element
199
- * @param {HTMLElement} button - Trigger button
200
- * @param {Function} onClose - Close callback
201
- * @returns {Function} Cleanup function
202
- */
203
- export function setupSettingsMenuKeyboard(menu: HTMLElement, button: HTMLElement | null, onClose?: () => void) {
204
- if (!menu) return () => {};
205
-
206
- const handleKeyDown = (e: KeyboardEvent) => {
207
- const menuItems = Array.from(menu.querySelectorAll<HTMLElement>('[role="menuitem"]'));
208
- const currentIndex = menuItems.indexOf(document.activeElement as HTMLElement);
209
-
210
- switch (e.key) {
211
- case 'ArrowDown':
212
- e.preventDefault();
213
- const nextIndex = (currentIndex + 1) % menuItems.length;
214
- menuItems.forEach((item, idx) => {
215
- item.setAttribute('tabindex', idx === nextIndex ? '0' : '-1');
216
- });
217
- const nextItem = menuItems[nextIndex];
218
- if (nextItem) {
219
- nextItem.focus({ preventScroll: false });
220
- nextItem.scrollIntoView(reducedMotionScrollOptions('nearest'));
221
- }
222
- break;
223
-
224
- case 'ArrowUp':
225
- e.preventDefault();
226
- const prevIndex = (currentIndex - 1 + menuItems.length) % menuItems.length;
227
- menuItems.forEach((item, idx) => {
228
- item.setAttribute('tabindex', idx === prevIndex ? '0' : '-1');
229
- });
230
- const prevItem = menuItems[prevIndex];
231
- if (prevItem) {
232
- prevItem.focus({ preventScroll: false });
233
- prevItem.scrollIntoView(reducedMotionScrollOptions('nearest'));
234
- }
235
- break;
236
-
237
- case 'Home':
238
- e.preventDefault();
239
- menuItems.forEach((item, idx) => {
240
- item.setAttribute('tabindex', idx === 0 ? '0' : '-1');
241
- });
242
- const homeItem = menuItems[0];
243
- if (homeItem) {
244
- homeItem.focus({ preventScroll: false });
245
- homeItem.scrollIntoView(reducedMotionScrollOptions('nearest'));
246
- }
247
- break;
248
-
249
- case 'End':
250
- e.preventDefault();
251
- const lastIndex = menuItems.length - 1;
252
- menuItems.forEach((item, idx) => {
253
- item.setAttribute('tabindex', idx === lastIndex ? '0' : '-1');
254
- });
255
- const lastItem = menuItems[lastIndex];
256
- if (lastItem) {
257
- lastItem.focus({ preventScroll: false });
258
- lastItem.scrollIntoView(reducedMotionScrollOptions('nearest'));
259
- }
260
- break;
261
-
262
- case 'Escape':
263
- e.preventDefault();
264
- if (onClose) {
265
- onClose();
266
- }
267
- if (button) {
268
- button.focus();
269
- }
270
- break;
271
-
272
- case 'Enter':
273
- case ' ':
274
- e.preventDefault();
275
- if (document.activeElement && menuItems.includes(document.activeElement as HTMLElement)) {
276
- (document.activeElement as HTMLElement).click();
277
- }
278
- break;
279
- }
280
- };
281
-
282
- menu.addEventListener('keydown', handleKeyDown);
283
-
284
- // Return cleanup function
285
- return () => {
286
- menu.removeEventListener('keydown', handleKeyDown);
287
- };
288
- }
289
-
1
+ /**
2
+ * Settings Menu Factory
3
+ * Creates standardized popup settings menus with consistent styling
4
+ */
5
+
6
+ import { DOMUtils } from './DOMUtils.js';
7
+ import { createIconElement } from '../icons/Icons.js';
8
+ import { i18n } from '../i18n/i18n.js';
9
+ import { reducedMotionScrollOptions } from './PerformanceUtils.js';
10
+
11
+ interface SettingsMenuItemConfig {
12
+ icon?: string;
13
+ label: string;
14
+ onClick?: ((e: Event) => void) | null;
15
+ closeMenuOnClick?: boolean;
16
+ }
17
+
18
+ interface CreateSettingsMenuOptions {
19
+ classPrefix: string;
20
+ menuClass: string;
21
+ menuId: string;
22
+ items?: SettingsMenuItemConfig[];
23
+ }
24
+
25
+ interface CreateSettingsMenuItemOptions {
26
+ classPrefix: string;
27
+ itemClass: string;
28
+ icon?: string;
29
+ label: string;
30
+ onClick?: ((e: Event) => void) | null;
31
+ closeMenuOnClick?: boolean;
32
+ }
33
+
34
+ /**
35
+ * Create a popup settings menu
36
+ * @param {Object} options - Menu options
37
+ * @param {string} options.classPrefix - Class prefix
38
+ * @param {string} options.menuClass - Specific menu class name
39
+ * @param {string} options.menuId - Menu element ID
40
+ * @param {Array} options.items - Array of menu item definitions
41
+ * @returns {HTMLElement} Menu element
42
+ */
43
+ export function createSettingsMenu({
44
+ classPrefix,
45
+ menuClass,
46
+ menuId,
47
+ items = []
48
+ }: CreateSettingsMenuOptions): HTMLElement {
49
+ const menu = DOMUtils.createElement('div', {
50
+ className: `${classPrefix}-popup-settings-menu ${menuClass}`,
51
+ attributes: {
52
+ 'id': menuId,
53
+ 'role': 'menu',
54
+ 'aria-label': i18n.t('settings.title')
55
+ }
56
+ });
57
+
58
+ // Add menu items
59
+ items.forEach((item: SettingsMenuItemConfig) => {
60
+ const menuItem = createSettingsMenuItem({
61
+ classPrefix,
62
+ itemClass: `${menuClass}-item`,
63
+ ...item
64
+ });
65
+ menu.appendChild(menuItem);
66
+ });
67
+
68
+ return menu;
69
+ }
70
+
71
+ /**
72
+ * Create a settings menu item
73
+ * @param {Object} options - Item options
74
+ * @param {string} options.classPrefix - Class prefix
75
+ * @param {string} options.itemClass - Item class name
76
+ * @param {string} options.icon - Icon name
77
+ * @param {string} options.label - Item label (i18n key or text)
78
+ * @param {Function} options.onClick - Click handler
79
+ * @param {boolean} options.closeMenuOnClick - Whether to close menu after click
80
+ * @returns {HTMLElement} Menu item button
81
+ */
82
+ export function createSettingsMenuItem({
83
+ classPrefix,
84
+ itemClass,
85
+ icon,
86
+ label,
87
+ onClick = null,
88
+ closeMenuOnClick: _closeMenuOnClick = true
89
+ }: CreateSettingsMenuItemOptions): HTMLElement {
90
+ // Check if label is an i18n key
91
+ const isI18nKey = typeof label === 'string' &&
92
+ (label.startsWith('transcript.') ||
93
+ label.startsWith('player.') ||
94
+ label.startsWith('settings.') ||
95
+ label.startsWith('captions.'));
96
+ const labelText = isI18nKey ? (i18n.t(label) || label) : label;
97
+
98
+ const button = DOMUtils.createElement('button', {
99
+ className: `${classPrefix}-popup-settings-item ${itemClass}`,
100
+ attributes: {
101
+ 'type': 'button',
102
+ 'role': 'menuitem',
103
+ 'tabindex': '-1'
104
+ }
105
+ });
106
+
107
+ if (icon) {
108
+ button.appendChild(createIconElement(icon));
109
+ }
110
+
111
+ const text = DOMUtils.createElement('span', {
112
+ textContent: labelText
113
+ });
114
+ button.appendChild(text);
115
+
116
+ if (onClick) {
117
+ button.addEventListener('click', (e) => {
118
+ onClick(e);
119
+ // Menu closing is handled by the caller if needed
120
+ });
121
+ }
122
+
123
+ return button;
124
+ }
125
+
126
+ /**
127
+ * Show a popup settings menu
128
+ * @param {HTMLElement} menu - Menu element
129
+ * @param {HTMLElement} button - Trigger button
130
+ * @param {string} classPrefix - Class prefix
131
+ */
132
+ export function showSettingsMenu(menu: HTMLElement, button: HTMLElement, classPrefix: string): void {
133
+ if (!menu || !button) return;
134
+
135
+ // Position menu relative to button
136
+ menu.style.display = 'block';
137
+
138
+ // Check if menu would go off-screen
139
+ const menuRect = menu.getBoundingClientRect();
140
+ const buttonRect = button.getBoundingClientRect();
141
+ const viewportHeight = window.innerHeight;
142
+
143
+ // Position above or below based on space
144
+ if (buttonRect.top - menuRect.height < 0 && buttonRect.bottom + menuRect.height < viewportHeight) {
145
+ // Position below
146
+ menu.style.top = '100%';
147
+ menu.style.bottom = 'auto';
148
+ DOMUtils.removeClass(menu, `${classPrefix}-menu-above`);
149
+ } else {
150
+ // Position above
151
+ menu.style.bottom = '100%';
152
+ menu.style.top = 'auto';
153
+ DOMUtils.addClass(menu, `${classPrefix}-menu-above`);
154
+ }
155
+
156
+ // Focus first item
157
+ const firstItem = menu.querySelector('[role="menuitem"]') as HTMLElement | null;
158
+ if (firstItem) {
159
+ firstItem.setAttribute('tabindex', '0');
160
+ setTimeout(() => firstItem.focus(), 50);
161
+ }
162
+ }
163
+
164
+ /**
165
+ * Hide a popup settings menu
166
+ * @param {HTMLElement} menu - Menu element
167
+ * @param {HTMLElement} button - Trigger button (to return focus)
168
+ */
169
+ export function hideSettingsMenu(menu: HTMLElement, button: HTMLElement | null): void {
170
+ if (!menu) return;
171
+
172
+ menu.style.display = 'none';
173
+
174
+ // Return focus to trigger button
175
+ if (button && document.contains(button)) {
176
+ button.focus();
177
+ }
178
+ }
179
+
180
+ /**
181
+ * Toggle a popup settings menu
182
+ * @param {HTMLElement} menu - Menu element
183
+ * @param {HTMLElement} button - Trigger button
184
+ * @param {string} classPrefix - Class prefix
185
+ */
186
+ export function toggleSettingsMenu(menu: HTMLElement, button: HTMLElement | null, classPrefix: string): void {
187
+ if (!menu || !button) return;
188
+
189
+ if (menu.style.display === 'block') {
190
+ hideSettingsMenu(menu, button);
191
+ } else {
192
+ showSettingsMenu(menu, button, classPrefix);
193
+ }
194
+ }
195
+
196
+ /**
197
+ * Setup keyboard navigation for settings menu
198
+ * @param {HTMLElement} menu - Menu element
199
+ * @param {HTMLElement} button - Trigger button
200
+ * @param {Function} onClose - Close callback
201
+ * @returns {Function} Cleanup function
202
+ */
203
+ export function setupSettingsMenuKeyboard(menu: HTMLElement, button: HTMLElement | null, onClose?: () => void) {
204
+ if (!menu) return () => {};
205
+
206
+ const handleKeyDown = (e: KeyboardEvent) => {
207
+ const menuItems = Array.from(menu.querySelectorAll<HTMLElement>('[role="menuitem"]'));
208
+ const currentIndex = menuItems.indexOf(document.activeElement as HTMLElement);
209
+
210
+ switch (e.key) {
211
+ case 'ArrowDown':
212
+ e.preventDefault();
213
+ const nextIndex = (currentIndex + 1) % menuItems.length;
214
+ menuItems.forEach((item, idx) => {
215
+ item.setAttribute('tabindex', idx === nextIndex ? '0' : '-1');
216
+ });
217
+ const nextItem = menuItems[nextIndex];
218
+ if (nextItem) {
219
+ nextItem.focus({ preventScroll: false });
220
+ nextItem.scrollIntoView(reducedMotionScrollOptions('nearest'));
221
+ }
222
+ break;
223
+
224
+ case 'ArrowUp':
225
+ e.preventDefault();
226
+ const prevIndex = (currentIndex - 1 + menuItems.length) % menuItems.length;
227
+ menuItems.forEach((item, idx) => {
228
+ item.setAttribute('tabindex', idx === prevIndex ? '0' : '-1');
229
+ });
230
+ const prevItem = menuItems[prevIndex];
231
+ if (prevItem) {
232
+ prevItem.focus({ preventScroll: false });
233
+ prevItem.scrollIntoView(reducedMotionScrollOptions('nearest'));
234
+ }
235
+ break;
236
+
237
+ case 'Home':
238
+ e.preventDefault();
239
+ menuItems.forEach((item, idx) => {
240
+ item.setAttribute('tabindex', idx === 0 ? '0' : '-1');
241
+ });
242
+ const homeItem = menuItems[0];
243
+ if (homeItem) {
244
+ homeItem.focus({ preventScroll: false });
245
+ homeItem.scrollIntoView(reducedMotionScrollOptions('nearest'));
246
+ }
247
+ break;
248
+
249
+ case 'End':
250
+ e.preventDefault();
251
+ const lastIndex = menuItems.length - 1;
252
+ menuItems.forEach((item, idx) => {
253
+ item.setAttribute('tabindex', idx === lastIndex ? '0' : '-1');
254
+ });
255
+ const lastItem = menuItems[lastIndex];
256
+ if (lastItem) {
257
+ lastItem.focus({ preventScroll: false });
258
+ lastItem.scrollIntoView(reducedMotionScrollOptions('nearest'));
259
+ }
260
+ break;
261
+
262
+ case 'Escape':
263
+ e.preventDefault();
264
+ if (onClose) {
265
+ onClose();
266
+ }
267
+ if (button) {
268
+ button.focus();
269
+ }
270
+ break;
271
+
272
+ case 'Enter':
273
+ case ' ':
274
+ e.preventDefault();
275
+ if (document.activeElement && menuItems.includes(document.activeElement as HTMLElement)) {
276
+ (document.activeElement as HTMLElement).click();
277
+ }
278
+ break;
279
+ }
280
+ };
281
+
282
+ menu.addEventListener('keydown', handleKeyDown);
283
+
284
+ // Return cleanup function
285
+ return () => {
286
+ menu.removeEventListener('keydown', handleKeyDown);
287
+ };
288
+ }
289
+