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.
- package/README.md +946 -946
- package/dist/dev/{vidply.AudioDescriptionManager-R47Q4YR4.js → vidply.AudioDescriptionManager-6RFVF27Z.js} +275 -19
- package/dist/dev/vidply.AudioDescriptionManager-6RFVF27Z.js.map +7 -0
- package/dist/dev/{vidply.CaptionStyleMenu-7XHRBWUM.js → vidply.CaptionStyleMenu-R2SCQ6VP.js} +4 -4
- package/dist/dev/{vidply.DASHRenderer-PWEMC3BT.js → vidply.DASHRenderer-EOVEW73H.js} +3 -3
- package/dist/dev/vidply.DASHRenderer-EOVEW73H.js.map +7 -0
- package/dist/dev/{vidply.FloatingPlayerManager-PKNNNAS2.js → vidply.FloatingPlayerManager-V5XQWS75.js} +5 -5
- package/dist/dev/vidply.FloatingPlayerManager-V5XQWS75.js.map +7 -0
- package/dist/dev/{vidply.HLSRenderer-E7RQ74HI.js → vidply.HLSRenderer-ZBEFVH5W.js} +4 -4
- package/dist/dev/vidply.HLSRenderer-ZBEFVH5W.js.map +7 -0
- package/dist/dev/vidply.HTML5Renderer-P7ICKYU4.js +12 -0
- package/dist/dev/{vidply.SignLanguageManager-FET5FQZ7.js → vidply.SignLanguageManager-7IAJOVF6.js} +8 -8
- package/dist/dev/vidply.SignLanguageManager-7IAJOVF6.js.map +7 -0
- package/dist/dev/{vidply.SoundCloudRenderer-RVSJZODO.js → vidply.SoundCloudRenderer-OA7QQYGN.js} +3 -3
- package/dist/dev/vidply.SoundCloudRenderer-OA7QQYGN.js.map +7 -0
- package/dist/dev/{vidply.TranscriptManager-5GXOQCLX.js → vidply.TranscriptManager-BPM3T23L.js} +10 -10
- package/dist/dev/vidply.TranscriptManager-BPM3T23L.js.map +7 -0
- package/dist/dev/{vidply.VimeoRenderer-ER2LSBBW.js → vidply.VimeoRenderer-BRHFYPNJ.js} +3 -3
- package/dist/dev/vidply.VimeoRenderer-BRHFYPNJ.js.map +7 -0
- package/dist/dev/{vidply.YouTubeRenderer-NFTOWJAB.js → vidply.YouTubeRenderer-HRTN4EJ5.js} +3 -3
- package/dist/dev/vidply.YouTubeRenderer-HRTN4EJ5.js.map +7 -0
- package/dist/dev/{vidply.chunk-6RQA4ROD.js → vidply.chunk-5IK3IC5V.js} +2 -2
- package/dist/dev/{vidply.chunk-JJ47NO24.js → vidply.chunk-C55OBT5H.js} +2 -2
- package/dist/dev/vidply.chunk-C55OBT5H.js.map +7 -0
- package/dist/dev/{vidply.chunk-DZSDO3B7.js → vidply.chunk-HOYEMJSD.js} +2 -2
- package/dist/dev/{vidply.chunk-VMSQAZM7.js → vidply.chunk-JAKZNGOR.js} +2 -2
- package/dist/dev/{vidply.chunk-64WXMTGL.js → vidply.chunk-LDVFAFGC.js} +5 -5
- package/dist/dev/vidply.chunk-LDVFAFGC.js.map +7 -0
- package/dist/dev/{vidply.chunk-RZWZGDV7.js → vidply.chunk-NSMAZCMB.js} +6 -6
- package/dist/dev/vidply.chunk-NSMAZCMB.js.map +7 -0
- package/dist/dev/{vidply.chunk-KI7C3QVL.js → vidply.chunk-RQG3MA7W.js} +2 -2
- package/dist/dev/vidply.chunk-RQG3MA7W.js.map +7 -0
- package/dist/dev/{vidply.chunk-BNFIUL2Z.js → vidply.chunk-UHMVDBDB.js} +3 -3
- package/dist/dev/vidply.chunk-UHMVDBDB.js.map +7 -0
- package/dist/dev/{vidply.chunk-NQPURMQY.js → vidply.chunk-W5BR65JY.js} +6 -6
- package/dist/dev/vidply.chunk-W5BR65JY.js.map +7 -0
- package/dist/dev/{vidply.chunk-W25HUCHS.js → vidply.chunk-XAK3DPTO.js} +2 -2
- package/dist/dev/vidply.chunk-XAK3DPTO.js.map +7 -0
- package/dist/dev/{vidply.chunk-SK4V2DAU.js → vidply.chunk-YVCYQZJI.js} +3 -3
- package/dist/dev/{vidply.de-4YAT7FFM.js → vidply.de-5W5LBFSJ.js} +2 -2
- package/dist/dev/vidply.de-5W5LBFSJ.js.map +7 -0
- package/dist/dev/{vidply.es-LEWIRUBH.js → vidply.es-SHDAT6L6.js} +2 -2
- package/dist/dev/vidply.es-SHDAT6L6.js.map +7 -0
- package/dist/dev/vidply.esm.js +73 -34
- package/dist/dev/vidply.esm.js.map +2 -2
- package/dist/dev/{vidply.fr-Q4GV2NOE.js → vidply.fr-2JANOMNB.js} +2 -2
- package/dist/dev/vidply.fr-2JANOMNB.js.map +7 -0
- package/dist/dev/{vidply.ja-S6ITA6WU.js → vidply.ja-WWMSTEJH.js} +2 -2
- package/dist/dev/vidply.ja-WWMSTEJH.js.map +7 -0
- package/dist/legacy/vidply.js +345 -32
- package/dist/legacy/vidply.js.map +3 -3
- package/dist/legacy/vidply.min.js +2 -2
- package/dist/legacy/vidply.min.meta.json +66 -47
- package/dist/prod/vidply.AudioDescriptionManager-YTDVDQBO.min.js +6 -0
- package/dist/prod/{vidply.CaptionStyleMenu-M3KVQWB5.min.js → vidply.CaptionStyleMenu-2AFF3HF2.min.js} +2 -2
- package/dist/prod/{vidply.DASHRenderer-DN3S27AV.min.js → vidply.DASHRenderer-URDOTOKS.min.js} +2 -2
- package/dist/prod/{vidply.FloatingPlayerManager-SKAZ4J43.min.js → vidply.FloatingPlayerManager-QUBFK7CQ.min.js} +2 -2
- package/dist/prod/{vidply.HLSRenderer-62HH4YTK.min.js → vidply.HLSRenderer-46KNUVAB.min.js} +2 -2
- package/dist/prod/vidply.HTML5Renderer-7TQDMIGY.min.js +6 -0
- package/dist/prod/{vidply.SignLanguageManager-V3OBTSSF.min.js → vidply.SignLanguageManager-H4C36EVL.min.js} +2 -2
- package/dist/prod/{vidply.SoundCloudRenderer-CXRVZIAJ.min.js → vidply.SoundCloudRenderer-OYSWMW7D.min.js} +2 -2
- package/dist/prod/{vidply.TranscriptManager-SYM3DFUY.min.js → vidply.TranscriptManager-BA3DAS63.min.js} +2 -2
- package/dist/prod/{vidply.VimeoRenderer-72TWRYFQ.min.js → vidply.VimeoRenderer-NF3COTHU.min.js} +2 -2
- package/dist/prod/{vidply.YouTubeRenderer-UMHEV3KD.min.js → vidply.YouTubeRenderer-UCQKYIJ4.min.js} +2 -2
- package/dist/prod/{vidply.chunk-Z3YDSA7O.min.js → vidply.chunk-2HNCP64N.min.js} +1 -1
- package/dist/prod/{vidply.chunk-EKEYJWMG.min.js → vidply.chunk-6ECBIYVI.min.js} +2 -2
- package/dist/prod/{vidply.chunk-YEENJS3Q.min.js → vidply.chunk-7S5AFTST.min.js} +2 -2
- package/dist/prod/{vidply.chunk-P4UXOHWA.min.js → vidply.chunk-EUOA2DJK.min.js} +1 -1
- package/dist/prod/{vidply.chunk-2TB6MUHO.min.js → vidply.chunk-G6KGXNEW.min.js} +1 -1
- package/dist/prod/{vidply.chunk-ABPXVDLZ.min.js → vidply.chunk-JPMAK3NS.min.js} +1 -1
- package/dist/prod/{vidply.chunk-P3PQBY7N.min.js → vidply.chunk-L4O44K63.min.js} +1 -1
- package/dist/prod/{vidply.chunk-WX3MVEAL.min.js → vidply.chunk-M76ABMKB.min.js} +1 -1
- package/dist/prod/{vidply.chunk-TM6AXJVD.min.js → vidply.chunk-WRVDVD33.min.js} +2 -2
- package/dist/prod/{vidply.chunk-RI3X7WHK.min.js → vidply.chunk-X2FQEQYR.min.js} +2 -2
- package/dist/prod/{vidply.chunk-UK7YGOB5.min.js → vidply.chunk-XXUH7ADU.min.js} +2 -2
- package/dist/prod/{vidply.de-BCHXRQ53.min.js → vidply.de-JMCATTKZ.min.js} +1 -1
- package/dist/prod/{vidply.es-PICAKMZD.min.js → vidply.es-YVG37V7Z.min.js} +1 -1
- package/dist/prod/vidply.esm.min.js +2 -2
- package/dist/prod/{vidply.fr-ZILI7CDR.min.js → vidply.fr-OGKGN2R4.min.js} +1 -1
- package/dist/prod/{vidply.ja-5WQ4OK4J.min.js → vidply.ja-Z6DS7QLS.min.js} +1 -1
- package/dist/vidply.css +5796 -5796
- package/dist/vidply.esm.min.meta.json +148 -135
- package/package.json +7 -7
- package/src/controls/CaptionManager.ts +598 -598
- package/src/controls/ControlBar.ts +4159 -4159
- package/src/controls/KeyboardManager.ts +383 -383
- package/src/controls/SettingsDialog.ts +488 -488
- package/src/controls/TranscriptManager.ts +2426 -2426
- package/src/core/AudioDescriptionManager.ts +863 -774
- package/src/core/DescriptionSpeechManager.ts +232 -0
- package/src/core/FloatingPlayerManager.ts +674 -674
- package/src/core/LazyInit.ts +80 -80
- package/src/core/MetadataAlertsManager.ts +514 -514
- package/src/core/Player.ts +2915 -2872
- package/src/core/PosterManager.ts +169 -169
- package/src/core/PseudoFullscreen.ts +179 -179
- package/src/core/ResponsiveManager.ts +231 -231
- package/src/core/ResumeManager.ts +236 -236
- package/src/core/SignLanguageManager.ts +1308 -1308
- package/src/core/ThemeManager.ts +157 -157
- package/src/features/PlaylistManager.ts +1838 -1827
- package/src/i18n/i18n.ts +223 -223
- package/src/i18n/languages/de.ts +217 -217
- package/src/i18n/languages/en.ts +218 -218
- package/src/i18n/languages/es.ts +214 -214
- package/src/i18n/languages/fr.ts +214 -214
- package/src/i18n/languages/ja.ts +214 -214
- package/src/icons/Icons.ts +167 -167
- package/src/index.ts +196 -193
- package/src/renderers/DASHRenderer.ts +1077 -1077
- package/src/renderers/HLSRenderer.ts +944 -944
- package/src/renderers/HTML5Renderer.ts +389 -389
- package/src/renderers/SoundCloudRenderer.ts +437 -437
- package/src/renderers/VimeoRenderer.ts +298 -298
- package/src/renderers/YouTubeRenderer.ts +290 -290
- package/src/styles/vidply.css +5796 -5796
- package/src/types/events.ts +124 -122
- package/src/types/globals.d.ts +313 -313
- package/src/types/options.ts +185 -182
- package/src/types/renderer.ts +58 -58
- package/src/utils/DOMUtils.ts +281 -281
- package/src/utils/DownloadInfo.ts +171 -171
- package/src/utils/DraggablePanel.ts +539 -539
- package/src/utils/DraggablePanelMenu.ts +195 -195
- package/src/utils/DraggableResizable.ts +997 -997
- package/src/utils/EventEmitter.ts +59 -59
- package/src/utils/FormUtils.ts +95 -95
- package/src/utils/MenuFactory.ts +483 -483
- package/src/utils/Sanitize.ts +70 -70
- package/src/utils/SettingsMenuFactory.ts +289 -289
- package/src/utils/StorageManager.ts +287 -287
- package/src/utils/TrackLabelUtils.ts +52 -52
- package/src/utils/UrlSafe.ts +69 -69
- package/src/utils/VideoFrameCapture.ts +114 -114
- package/src/utils/WindowComponents.ts +250 -250
- package/dist/dev/vidply.AudioDescriptionManager-R47Q4YR4.js.map +0 -7
- package/dist/dev/vidply.DASHRenderer-PWEMC3BT.js.map +0 -7
- package/dist/dev/vidply.FloatingPlayerManager-PKNNNAS2.js.map +0 -7
- package/dist/dev/vidply.HLSRenderer-E7RQ74HI.js.map +0 -7
- package/dist/dev/vidply.HTML5Renderer-BU4QJJJY.js +0 -12
- package/dist/dev/vidply.SignLanguageManager-FET5FQZ7.js.map +0 -7
- package/dist/dev/vidply.SoundCloudRenderer-RVSJZODO.js.map +0 -7
- package/dist/dev/vidply.TranscriptManager-5GXOQCLX.js.map +0 -7
- package/dist/dev/vidply.VimeoRenderer-ER2LSBBW.js.map +0 -7
- package/dist/dev/vidply.YouTubeRenderer-NFTOWJAB.js.map +0 -7
- package/dist/dev/vidply.chunk-64WXMTGL.js.map +0 -7
- package/dist/dev/vidply.chunk-BNFIUL2Z.js.map +0 -7
- package/dist/dev/vidply.chunk-JJ47NO24.js.map +0 -7
- package/dist/dev/vidply.chunk-KI7C3QVL.js.map +0 -7
- package/dist/dev/vidply.chunk-NQPURMQY.js.map +0 -7
- package/dist/dev/vidply.chunk-RZWZGDV7.js.map +0 -7
- package/dist/dev/vidply.chunk-W25HUCHS.js.map +0 -7
- package/dist/dev/vidply.de-4YAT7FFM.js.map +0 -7
- package/dist/dev/vidply.es-LEWIRUBH.js.map +0 -7
- package/dist/dev/vidply.fr-Q4GV2NOE.js.map +0 -7
- package/dist/dev/vidply.ja-S6ITA6WU.js.map +0 -7
- package/dist/prod/vidply.AudioDescriptionManager-RHLIJW5C.min.js +0 -6
- package/dist/prod/vidply.HTML5Renderer-IU5T3WEW.min.js +0 -6
- /package/dist/dev/{vidply.CaptionStyleMenu-7XHRBWUM.js.map → vidply.CaptionStyleMenu-R2SCQ6VP.js.map} +0 -0
- /package/dist/dev/{vidply.HTML5Renderer-BU4QJJJY.js.map → vidply.HTML5Renderer-P7ICKYU4.js.map} +0 -0
- /package/dist/dev/{vidply.chunk-6RQA4ROD.js.map → vidply.chunk-5IK3IC5V.js.map} +0 -0
- /package/dist/dev/{vidply.chunk-DZSDO3B7.js.map → vidply.chunk-HOYEMJSD.js.map} +0 -0
- /package/dist/dev/{vidply.chunk-VMSQAZM7.js.map → vidply.chunk-JAKZNGOR.js.map} +0 -0
- /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
|
+
|