vidply 1.2.0 → 1.2.1

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 (164) hide show
  1. package/README.md +946 -946
  2. package/dist/dev/{vidply.AudioDescriptionManager-R47Q4YR4.js → vidply.AudioDescriptionManager-6RFVF27Z.js} +275 -19
  3. package/dist/dev/vidply.AudioDescriptionManager-6RFVF27Z.js.map +7 -0
  4. package/dist/dev/{vidply.CaptionStyleMenu-7XHRBWUM.js → vidply.CaptionStyleMenu-R2SCQ6VP.js} +4 -4
  5. package/dist/dev/{vidply.DASHRenderer-PWEMC3BT.js → vidply.DASHRenderer-EOVEW73H.js} +3 -3
  6. package/dist/dev/vidply.DASHRenderer-EOVEW73H.js.map +7 -0
  7. package/dist/dev/{vidply.FloatingPlayerManager-PKNNNAS2.js → vidply.FloatingPlayerManager-V5XQWS75.js} +5 -5
  8. package/dist/dev/vidply.FloatingPlayerManager-V5XQWS75.js.map +7 -0
  9. package/dist/dev/{vidply.HLSRenderer-E7RQ74HI.js → vidply.HLSRenderer-ZBEFVH5W.js} +4 -4
  10. package/dist/dev/vidply.HLSRenderer-ZBEFVH5W.js.map +7 -0
  11. package/dist/dev/vidply.HTML5Renderer-P7ICKYU4.js +12 -0
  12. package/dist/dev/{vidply.SignLanguageManager-FET5FQZ7.js → vidply.SignLanguageManager-7IAJOVF6.js} +8 -8
  13. package/dist/dev/vidply.SignLanguageManager-7IAJOVF6.js.map +7 -0
  14. package/dist/dev/{vidply.SoundCloudRenderer-RVSJZODO.js → vidply.SoundCloudRenderer-OA7QQYGN.js} +3 -3
  15. package/dist/dev/vidply.SoundCloudRenderer-OA7QQYGN.js.map +7 -0
  16. package/dist/dev/{vidply.TranscriptManager-5GXOQCLX.js → vidply.TranscriptManager-BPM3T23L.js} +10 -10
  17. package/dist/dev/vidply.TranscriptManager-BPM3T23L.js.map +7 -0
  18. package/dist/dev/{vidply.VimeoRenderer-ER2LSBBW.js → vidply.VimeoRenderer-BRHFYPNJ.js} +3 -3
  19. package/dist/dev/vidply.VimeoRenderer-BRHFYPNJ.js.map +7 -0
  20. package/dist/dev/{vidply.YouTubeRenderer-NFTOWJAB.js → vidply.YouTubeRenderer-HRTN4EJ5.js} +3 -3
  21. package/dist/dev/vidply.YouTubeRenderer-HRTN4EJ5.js.map +7 -0
  22. package/dist/dev/{vidply.chunk-6RQA4ROD.js → vidply.chunk-5IK3IC5V.js} +2 -2
  23. package/dist/dev/{vidply.chunk-JJ47NO24.js → vidply.chunk-C55OBT5H.js} +2 -2
  24. package/dist/dev/vidply.chunk-C55OBT5H.js.map +7 -0
  25. package/dist/dev/{vidply.chunk-DZSDO3B7.js → vidply.chunk-HOYEMJSD.js} +2 -2
  26. package/dist/dev/{vidply.chunk-VMSQAZM7.js → vidply.chunk-JAKZNGOR.js} +2 -2
  27. package/dist/dev/{vidply.chunk-64WXMTGL.js → vidply.chunk-LDVFAFGC.js} +5 -5
  28. package/dist/dev/vidply.chunk-LDVFAFGC.js.map +7 -0
  29. package/dist/dev/{vidply.chunk-RZWZGDV7.js → vidply.chunk-NSMAZCMB.js} +6 -6
  30. package/dist/dev/vidply.chunk-NSMAZCMB.js.map +7 -0
  31. package/dist/dev/{vidply.chunk-KI7C3QVL.js → vidply.chunk-RQG3MA7W.js} +2 -2
  32. package/dist/dev/vidply.chunk-RQG3MA7W.js.map +7 -0
  33. package/dist/dev/{vidply.chunk-BNFIUL2Z.js → vidply.chunk-UHMVDBDB.js} +3 -3
  34. package/dist/dev/vidply.chunk-UHMVDBDB.js.map +7 -0
  35. package/dist/dev/{vidply.chunk-NQPURMQY.js → vidply.chunk-W5BR65JY.js} +6 -6
  36. package/dist/dev/vidply.chunk-W5BR65JY.js.map +7 -0
  37. package/dist/dev/{vidply.chunk-W25HUCHS.js → vidply.chunk-XAK3DPTO.js} +2 -2
  38. package/dist/dev/vidply.chunk-XAK3DPTO.js.map +7 -0
  39. package/dist/dev/{vidply.chunk-SK4V2DAU.js → vidply.chunk-YVCYQZJI.js} +3 -3
  40. package/dist/dev/{vidply.de-4YAT7FFM.js → vidply.de-5W5LBFSJ.js} +2 -2
  41. package/dist/dev/vidply.de-5W5LBFSJ.js.map +7 -0
  42. package/dist/dev/{vidply.es-LEWIRUBH.js → vidply.es-SHDAT6L6.js} +2 -2
  43. package/dist/dev/vidply.es-SHDAT6L6.js.map +7 -0
  44. package/dist/dev/vidply.esm.js +73 -34
  45. package/dist/dev/vidply.esm.js.map +2 -2
  46. package/dist/dev/{vidply.fr-Q4GV2NOE.js → vidply.fr-2JANOMNB.js} +2 -2
  47. package/dist/dev/vidply.fr-2JANOMNB.js.map +7 -0
  48. package/dist/dev/{vidply.ja-S6ITA6WU.js → vidply.ja-WWMSTEJH.js} +2 -2
  49. package/dist/dev/vidply.ja-WWMSTEJH.js.map +7 -0
  50. package/dist/legacy/vidply.js +345 -32
  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 +66 -47
  54. package/dist/prod/vidply.AudioDescriptionManager-YTDVDQBO.min.js +6 -0
  55. package/dist/prod/{vidply.CaptionStyleMenu-M3KVQWB5.min.js → vidply.CaptionStyleMenu-2AFF3HF2.min.js} +2 -2
  56. package/dist/prod/{vidply.DASHRenderer-DN3S27AV.min.js → vidply.DASHRenderer-URDOTOKS.min.js} +2 -2
  57. package/dist/prod/{vidply.FloatingPlayerManager-SKAZ4J43.min.js → vidply.FloatingPlayerManager-QUBFK7CQ.min.js} +2 -2
  58. package/dist/prod/{vidply.HLSRenderer-62HH4YTK.min.js → vidply.HLSRenderer-46KNUVAB.min.js} +2 -2
  59. package/dist/prod/vidply.HTML5Renderer-7TQDMIGY.min.js +6 -0
  60. package/dist/prod/{vidply.SignLanguageManager-V3OBTSSF.min.js → vidply.SignLanguageManager-H4C36EVL.min.js} +2 -2
  61. package/dist/prod/{vidply.SoundCloudRenderer-CXRVZIAJ.min.js → vidply.SoundCloudRenderer-OYSWMW7D.min.js} +2 -2
  62. package/dist/prod/{vidply.TranscriptManager-SYM3DFUY.min.js → vidply.TranscriptManager-BA3DAS63.min.js} +2 -2
  63. package/dist/prod/{vidply.VimeoRenderer-72TWRYFQ.min.js → vidply.VimeoRenderer-NF3COTHU.min.js} +2 -2
  64. package/dist/prod/{vidply.YouTubeRenderer-UMHEV3KD.min.js → vidply.YouTubeRenderer-UCQKYIJ4.min.js} +2 -2
  65. package/dist/prod/{vidply.chunk-Z3YDSA7O.min.js → vidply.chunk-2HNCP64N.min.js} +1 -1
  66. package/dist/prod/{vidply.chunk-EKEYJWMG.min.js → vidply.chunk-6ECBIYVI.min.js} +2 -2
  67. package/dist/prod/{vidply.chunk-YEENJS3Q.min.js → vidply.chunk-7S5AFTST.min.js} +2 -2
  68. package/dist/prod/{vidply.chunk-P4UXOHWA.min.js → vidply.chunk-EUOA2DJK.min.js} +1 -1
  69. package/dist/prod/{vidply.chunk-2TB6MUHO.min.js → vidply.chunk-G6KGXNEW.min.js} +1 -1
  70. package/dist/prod/{vidply.chunk-ABPXVDLZ.min.js → vidply.chunk-JPMAK3NS.min.js} +1 -1
  71. package/dist/prod/{vidply.chunk-P3PQBY7N.min.js → vidply.chunk-L4O44K63.min.js} +1 -1
  72. package/dist/prod/{vidply.chunk-WX3MVEAL.min.js → vidply.chunk-M76ABMKB.min.js} +1 -1
  73. package/dist/prod/{vidply.chunk-TM6AXJVD.min.js → vidply.chunk-WRVDVD33.min.js} +2 -2
  74. package/dist/prod/{vidply.chunk-RI3X7WHK.min.js → vidply.chunk-X2FQEQYR.min.js} +2 -2
  75. package/dist/prod/{vidply.chunk-UK7YGOB5.min.js → vidply.chunk-XXUH7ADU.min.js} +2 -2
  76. package/dist/prod/{vidply.de-BCHXRQ53.min.js → vidply.de-JMCATTKZ.min.js} +1 -1
  77. package/dist/prod/{vidply.es-PICAKMZD.min.js → vidply.es-YVG37V7Z.min.js} +1 -1
  78. package/dist/prod/vidply.esm.min.js +2 -2
  79. package/dist/prod/{vidply.fr-ZILI7CDR.min.js → vidply.fr-OGKGN2R4.min.js} +1 -1
  80. package/dist/prod/{vidply.ja-5WQ4OK4J.min.js → vidply.ja-Z6DS7QLS.min.js} +1 -1
  81. package/dist/vidply.css +5796 -5796
  82. package/dist/vidply.esm.min.meta.json +148 -135
  83. package/package.json +7 -7
  84. package/src/controls/CaptionManager.ts +598 -598
  85. package/src/controls/ControlBar.ts +4159 -4159
  86. package/src/controls/KeyboardManager.ts +383 -383
  87. package/src/controls/SettingsDialog.ts +488 -488
  88. package/src/controls/TranscriptManager.ts +2426 -2426
  89. package/src/core/AudioDescriptionManager.ts +863 -774
  90. package/src/core/DescriptionSpeechManager.ts +232 -0
  91. package/src/core/FloatingPlayerManager.ts +674 -674
  92. package/src/core/LazyInit.ts +80 -80
  93. package/src/core/MetadataAlertsManager.ts +514 -514
  94. package/src/core/Player.ts +2915 -2872
  95. package/src/core/PosterManager.ts +169 -169
  96. package/src/core/PseudoFullscreen.ts +179 -179
  97. package/src/core/ResponsiveManager.ts +231 -231
  98. package/src/core/ResumeManager.ts +236 -236
  99. package/src/core/SignLanguageManager.ts +1308 -1308
  100. package/src/core/ThemeManager.ts +157 -157
  101. package/src/features/PlaylistManager.ts +1838 -1827
  102. package/src/i18n/i18n.ts +223 -223
  103. package/src/i18n/languages/de.ts +217 -217
  104. package/src/i18n/languages/en.ts +218 -218
  105. package/src/i18n/languages/es.ts +214 -214
  106. package/src/i18n/languages/fr.ts +214 -214
  107. package/src/i18n/languages/ja.ts +214 -214
  108. package/src/icons/Icons.ts +167 -167
  109. package/src/index.ts +196 -193
  110. package/src/renderers/DASHRenderer.ts +1077 -1077
  111. package/src/renderers/HLSRenderer.ts +944 -944
  112. package/src/renderers/HTML5Renderer.ts +389 -389
  113. package/src/renderers/SoundCloudRenderer.ts +437 -437
  114. package/src/renderers/VimeoRenderer.ts +298 -298
  115. package/src/renderers/YouTubeRenderer.ts +290 -290
  116. package/src/styles/vidply.css +5796 -5796
  117. package/src/types/events.ts +124 -122
  118. package/src/types/globals.d.ts +313 -313
  119. package/src/types/options.ts +185 -182
  120. package/src/types/renderer.ts +58 -58
  121. package/src/utils/DOMUtils.ts +281 -281
  122. package/src/utils/DownloadInfo.ts +171 -171
  123. package/src/utils/DraggablePanel.ts +539 -539
  124. package/src/utils/DraggablePanelMenu.ts +195 -195
  125. package/src/utils/DraggableResizable.ts +997 -997
  126. package/src/utils/EventEmitter.ts +59 -59
  127. package/src/utils/FormUtils.ts +95 -95
  128. package/src/utils/MenuFactory.ts +483 -483
  129. package/src/utils/Sanitize.ts +70 -70
  130. package/src/utils/SettingsMenuFactory.ts +289 -289
  131. package/src/utils/StorageManager.ts +287 -287
  132. package/src/utils/TrackLabelUtils.ts +52 -52
  133. package/src/utils/UrlSafe.ts +69 -69
  134. package/src/utils/VideoFrameCapture.ts +114 -114
  135. package/src/utils/WindowComponents.ts +250 -250
  136. package/dist/dev/vidply.AudioDescriptionManager-R47Q4YR4.js.map +0 -7
  137. package/dist/dev/vidply.DASHRenderer-PWEMC3BT.js.map +0 -7
  138. package/dist/dev/vidply.FloatingPlayerManager-PKNNNAS2.js.map +0 -7
  139. package/dist/dev/vidply.HLSRenderer-E7RQ74HI.js.map +0 -7
  140. package/dist/dev/vidply.HTML5Renderer-BU4QJJJY.js +0 -12
  141. package/dist/dev/vidply.SignLanguageManager-FET5FQZ7.js.map +0 -7
  142. package/dist/dev/vidply.SoundCloudRenderer-RVSJZODO.js.map +0 -7
  143. package/dist/dev/vidply.TranscriptManager-5GXOQCLX.js.map +0 -7
  144. package/dist/dev/vidply.VimeoRenderer-ER2LSBBW.js.map +0 -7
  145. package/dist/dev/vidply.YouTubeRenderer-NFTOWJAB.js.map +0 -7
  146. package/dist/dev/vidply.chunk-64WXMTGL.js.map +0 -7
  147. package/dist/dev/vidply.chunk-BNFIUL2Z.js.map +0 -7
  148. package/dist/dev/vidply.chunk-JJ47NO24.js.map +0 -7
  149. package/dist/dev/vidply.chunk-KI7C3QVL.js.map +0 -7
  150. package/dist/dev/vidply.chunk-NQPURMQY.js.map +0 -7
  151. package/dist/dev/vidply.chunk-RZWZGDV7.js.map +0 -7
  152. package/dist/dev/vidply.chunk-W25HUCHS.js.map +0 -7
  153. package/dist/dev/vidply.de-4YAT7FFM.js.map +0 -7
  154. package/dist/dev/vidply.es-LEWIRUBH.js.map +0 -7
  155. package/dist/dev/vidply.fr-Q4GV2NOE.js.map +0 -7
  156. package/dist/dev/vidply.ja-S6ITA6WU.js.map +0 -7
  157. package/dist/prod/vidply.AudioDescriptionManager-RHLIJW5C.min.js +0 -6
  158. package/dist/prod/vidply.HTML5Renderer-IU5T3WEW.min.js +0 -6
  159. /package/dist/dev/{vidply.CaptionStyleMenu-7XHRBWUM.js.map → vidply.CaptionStyleMenu-R2SCQ6VP.js.map} +0 -0
  160. /package/dist/dev/{vidply.HTML5Renderer-BU4QJJJY.js.map → vidply.HTML5Renderer-P7ICKYU4.js.map} +0 -0
  161. /package/dist/dev/{vidply.chunk-6RQA4ROD.js.map → vidply.chunk-5IK3IC5V.js.map} +0 -0
  162. /package/dist/dev/{vidply.chunk-DZSDO3B7.js.map → vidply.chunk-HOYEMJSD.js.map} +0 -0
  163. /package/dist/dev/{vidply.chunk-VMSQAZM7.js.map → vidply.chunk-JAKZNGOR.js.map} +0 -0
  164. /package/dist/dev/{vidply.chunk-SK4V2DAU.js.map → vidply.chunk-YVCYQZJI.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
+