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
@@ -0,0 +1,326 @@
1
+ /**
2
+ * Keyboard Shortcuts Help Dialog
3
+ *
4
+ * A focus-trapped modal listing the player's active keyboard shortcuts.
5
+ * Built lazily from `player.options.keyboardShortcuts` so it always reflects
6
+ * the live bindings (including consumer overrides). Mirrors the focus, escape
7
+ * and return-focus behaviour of SettingsDialog for a consistent, accessible UX.
8
+ */
9
+
10
+ import { DOMUtils } from '../utils/DOMUtils.js';
11
+ import { createIconElement } from '../icons/Icons.js';
12
+ import { i18n } from '../i18n/i18n.js';
13
+ import type { Player } from '../core/Player.js';
14
+ import type { KeyboardShortcuts } from '../types/options.js';
15
+
16
+ /**
17
+ * Display order for the shortcut rows. Only actions that are both listed here
18
+ * and have at least one bound key are rendered, so removing a binding hides
19
+ * its row automatically.
20
+ */
21
+ const ACTION_ORDER: Array<keyof KeyboardShortcuts> = [
22
+ 'play-pause',
23
+ 'seek-backward',
24
+ 'seek-forward',
25
+ 'volume-up',
26
+ 'volume-down',
27
+ 'mute',
28
+ 'captions',
29
+ 'caption-style-menu',
30
+ 'speed-down',
31
+ 'speed-up',
32
+ 'speed-menu',
33
+ 'quality-menu',
34
+ 'chapters-menu',
35
+ 'transcript-toggle',
36
+ 'fullscreen',
37
+ 'help'
38
+ ];
39
+
40
+ /**
41
+ * Feature-specific actions only make sense when the matching feature actually
42
+ * exists on this player (e.g. there's no point listing the captions shortcut
43
+ * for a video without captions). Each value is the key under
44
+ * `player.controlBar.controls` whose presence proves the feature is available
45
+ * and enabled for this player. Actions not listed here (play/pause, seek,
46
+ * volume, mute, help) are always relevant for any audio/video player.
47
+ */
48
+ const ACTION_REQUIRES_CONTROL: Partial<Record<keyof KeyboardShortcuts, string>> = {
49
+ captions: 'captions',
50
+ 'caption-style-menu': 'captionStyle',
51
+ 'speed-down': 'speed',
52
+ 'speed-up': 'speed',
53
+ 'speed-menu': 'speed',
54
+ 'quality-menu': 'quality',
55
+ 'chapters-menu': 'chapters',
56
+ 'transcript-toggle': 'transcript',
57
+ fullscreen: 'fullscreen'
58
+ };
59
+
60
+ export class KeyboardHelp {
61
+ player: Player;
62
+ isOpen = false;
63
+ overlay: HTMLElement | null = null;
64
+ private _triggerElement: HTMLElement | null = null;
65
+ private _keydownHandler: ((e: KeyboardEvent) => void) | null = null;
66
+ private _content: HTMLElement | null = null;
67
+
68
+ constructor(player: Player) {
69
+ this.player = player;
70
+ }
71
+
72
+ private get prefix(): string {
73
+ return this.player.options.classPrefix;
74
+ }
75
+
76
+ /**
77
+ * Turn a raw KeyboardEvent.key value into a human-readable label. Arrow
78
+ * keys become universally understood glyphs; the space bar and single
79
+ * letters are normalised for legibility.
80
+ */
81
+ private formatKey(key: string): string {
82
+ switch (key) {
83
+ case ' ':
84
+ return i18n.t('help.keys.space');
85
+ case 'ArrowUp':
86
+ return '\u2191';
87
+ case 'ArrowDown':
88
+ return '\u2193';
89
+ case 'ArrowLeft':
90
+ return '\u2190';
91
+ case 'ArrowRight':
92
+ return '\u2192';
93
+ case 'Escape':
94
+ return 'Esc';
95
+ default:
96
+ return key.length === 1 ? key.toUpperCase() : key;
97
+ }
98
+ }
99
+
100
+ private createElement(): HTMLElement {
101
+ const titleId = `${this.prefix}-help-title-${this.player.instanceId}`;
102
+
103
+ const overlay = DOMUtils.createElement('div', {
104
+ className: `${this.prefix}-settings-overlay ${this.prefix}-help-overlay`,
105
+ attributes: {
106
+ role: 'dialog',
107
+ 'aria-modal': 'true',
108
+ 'aria-labelledby': titleId
109
+ }
110
+ });
111
+ overlay.style.display = 'none';
112
+
113
+ const dialog = DOMUtils.createElement('div', {
114
+ className: `${this.prefix}-settings-dialog ${this.prefix}-help-dialog`
115
+ });
116
+
117
+ const header = DOMUtils.createElement('div', {
118
+ className: `${this.prefix}-settings-header`
119
+ });
120
+
121
+ const title = DOMUtils.createElement('h2', {
122
+ textContent: i18n.t('help.title'),
123
+ attributes: { id: titleId }
124
+ });
125
+
126
+ const closeButton = DOMUtils.createElement('button', {
127
+ className: `${this.prefix}-button ${this.prefix}-settings-close`,
128
+ attributes: {
129
+ type: 'button',
130
+ 'aria-label': i18n.t('help.close')
131
+ }
132
+ });
133
+ closeButton.appendChild(createIconElement('close'));
134
+ closeButton.addEventListener('click', () => this.hide());
135
+
136
+ header.appendChild(title);
137
+ header.appendChild(closeButton);
138
+
139
+ const content = DOMUtils.createElement('div', {
140
+ className: `${this.prefix}-settings-content`
141
+ });
142
+ this._content = content;
143
+ content.appendChild(this.buildShortcutList());
144
+
145
+ dialog.appendChild(header);
146
+ dialog.appendChild(content);
147
+ overlay.appendChild(dialog);
148
+
149
+ overlay.addEventListener('click', (e: MouseEvent) => {
150
+ if (e.target === overlay) {
151
+ this.hide();
152
+ }
153
+ });
154
+
155
+ this._keydownHandler = (e: KeyboardEvent) => {
156
+ if (!this.isOpen || !this.overlay) return;
157
+
158
+ if (e.key === 'Escape') {
159
+ e.preventDefault();
160
+ this.hide();
161
+ return;
162
+ }
163
+
164
+ // Trap Tab within the dialog (WCAG 2.1.2, 2.4.3).
165
+ if (e.key === 'Tab') {
166
+ const focusable = Array.from(
167
+ this.overlay.querySelectorAll<HTMLElement>(
168
+ 'button, [href], select, input, textarea, [tabindex]:not([tabindex="-1"])'
169
+ )
170
+ ).filter((el) => !el.hasAttribute('disabled') && el.offsetParent !== null);
171
+ if (focusable.length === 0) return;
172
+
173
+ const first = focusable[0];
174
+ const last = focusable[focusable.length - 1];
175
+ if (!first || !last) return;
176
+ const active = document.activeElement as HTMLElement | null;
177
+
178
+ if (e.shiftKey && (active === first || !this.overlay.contains(active))) {
179
+ e.preventDefault();
180
+ last.focus({ preventScroll: true });
181
+ } else if (!e.shiftKey && active === last) {
182
+ e.preventDefault();
183
+ first.focus({ preventScroll: true });
184
+ }
185
+ }
186
+ };
187
+ const lifecycleSignal = (this.player as { lifecycleSignal?: AbortSignal }).lifecycleSignal;
188
+ document.addEventListener(
189
+ 'keydown',
190
+ this._keydownHandler,
191
+ lifecycleSignal ? { signal: lifecycleSignal } : undefined
192
+ );
193
+
194
+ return overlay;
195
+ }
196
+
197
+ /**
198
+ * Whether a shortcut row is worth showing for *this* player. Feature actions
199
+ * are hidden when their control isn't present (e.g. no captions track, an
200
+ * audio-only player with no fullscreen). Core actions are always relevant.
201
+ *
202
+ * When the player has no control bar we can't infer availability, so nothing
203
+ * is hidden — the shortcuts still work and we'd rather over-show than mislead.
204
+ */
205
+ private isActionRelevant(action: keyof KeyboardShortcuts): boolean {
206
+ const requiredControl = ACTION_REQUIRES_CONTROL[action];
207
+ if (!requiredControl) return true;
208
+
209
+ const controlBar = (this.player as { controlBar?: { controls?: Record<string, unknown> } }).controlBar;
210
+ if (!controlBar || !controlBar.controls) return true;
211
+
212
+ return Boolean(controlBar.controls[requiredControl]);
213
+ }
214
+
215
+ private buildShortcutList(): HTMLElement {
216
+ const list = DOMUtils.createElement('dl', {
217
+ className: `${this.prefix}-help-list`
218
+ });
219
+
220
+ const shortcuts = this.player.options.keyboardShortcuts as unknown as Record<string, string[] | undefined>;
221
+
222
+ for (const action of ACTION_ORDER) {
223
+ const keys = shortcuts[action];
224
+ if (!Array.isArray(keys) || keys.length === 0) continue;
225
+ if (!this.isActionRelevant(action)) continue;
226
+
227
+ const term = DOMUtils.createElement('dt', {
228
+ className: `${this.prefix}-help-action`,
229
+ textContent: i18n.t(`help.actions.${action}`)
230
+ });
231
+
232
+ const desc = DOMUtils.createElement('dd', {
233
+ className: `${this.prefix}-help-keys`
234
+ });
235
+
236
+ keys.forEach((key, index) => {
237
+ if (index > 0) {
238
+ desc.appendChild(
239
+ DOMUtils.createElement('span', {
240
+ className: `${this.prefix}-help-key-sep`,
241
+ textContent: i18n.t('help.or')
242
+ })
243
+ );
244
+ }
245
+ desc.appendChild(
246
+ DOMUtils.createElement('kbd', {
247
+ className: `${this.prefix}-help-key`,
248
+ textContent: this.formatKey(key)
249
+ })
250
+ );
251
+ });
252
+
253
+ list.appendChild(term);
254
+ list.appendChild(desc);
255
+ }
256
+
257
+ return list;
258
+ }
259
+
260
+ show(): void {
261
+ if (this.isOpen) return;
262
+
263
+ if (!this.overlay) {
264
+ this.overlay = this.createElement();
265
+ this.player.container.appendChild(this.overlay);
266
+ } else if (this._content) {
267
+ // Rebuild so feature availability that changed since last open
268
+ // (e.g. HLS captions/qualities loaded later) is reflected.
269
+ this._content.replaceChildren(this.buildShortcutList());
270
+ }
271
+
272
+ const active = (typeof document !== 'undefined' ? document.activeElement : null) as HTMLElement | null;
273
+ this._triggerElement = active && typeof active.focus === 'function' ? active : null;
274
+
275
+ this.overlay.style.display = 'flex';
276
+ this.isOpen = true;
277
+
278
+ const closeButton = this.overlay.querySelector<HTMLElement>(`.${this.prefix}-settings-close`);
279
+ closeButton?.focus({ preventScroll: true });
280
+
281
+ this.player.emit('keyboardhelpopen');
282
+ }
283
+
284
+ hide(): void {
285
+ if (!this.overlay) return;
286
+
287
+ this.overlay.style.display = 'none';
288
+ this.isOpen = false;
289
+
290
+ const trigger = this._triggerElement;
291
+ this._triggerElement = null;
292
+ if (trigger && document.contains(trigger)) {
293
+ try {
294
+ trigger.focus({ preventScroll: true });
295
+ } catch {
296
+ this.player.container?.focus();
297
+ }
298
+ } else {
299
+ this.player.container?.focus();
300
+ }
301
+
302
+ this.player.emit('keyboardhelpclose');
303
+ }
304
+
305
+ toggle(): void {
306
+ if (this.isOpen) {
307
+ this.hide();
308
+ } else {
309
+ this.show();
310
+ }
311
+ }
312
+
313
+ destroy(): void {
314
+ if (this._keydownHandler) {
315
+ document.removeEventListener('keydown', this._keydownHandler);
316
+ this._keydownHandler = null;
317
+ }
318
+ if (this.overlay && this.overlay.parentNode) {
319
+ this.overlay.parentNode.removeChild(this.overlay);
320
+ }
321
+ this.overlay = null;
322
+ this._content = null;
323
+ this._triggerElement = null;
324
+ this.isOpen = false;
325
+ }
326
+ }