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,195 +1,195 @@
1
- /**
2
- * Shared helpers for the settings-menu items that toggle drag / resize
3
- * mode on a `DraggableResizable`-powered panel.
4
- *
5
- * Previously `SignLanguageManager` and `TranscriptManager` both carried
6
- * near-identical copies of:
7
- * - `updateDragOptionState` / `updateResizeOptionState`
8
- * (button `aria-checked` + label + i18n-driven text sync)
9
- * - `positionSettingsMenuImmediate`
10
- * (place the menu relative to the settings button, with above/below
11
- * flipping and viewport-edge clamping)
12
- *
13
- * This module owns those pieces so a future manager (or a refactor of
14
- * an existing one) doesn't have to re-derive the rules from scratch.
15
- *
16
- * NOTE: the full "DraggablePanel" abstraction that also owns the menu
17
- * *contents* (language selector, style dialog, pinned state, etc.) is
18
- * deliberately NOT attempted here — the per-manager specifics are
19
- * significant and an attempt to unify them would churn security-
20
- * sensitive code without meaningfully reducing LOC.
21
- */
22
-
23
- /** Visual state of a two-state ("currently enabled / currently disabled")
24
- * toggle menu item. */
25
- export interface ToggleMenuItemState {
26
- /** Whether the underlying mode is currently active. */
27
- enabled: boolean;
28
- /** Label shown when the mode is active (i.e. the label invites the
29
- * user to disable it). */
30
- enabledText: string;
31
- /** Label shown when the mode is inactive. */
32
- disabledText: string;
33
- /** `aria-label` for screen readers when the mode is active. */
34
- enabledAria: string;
35
- /** `aria-label` for screen readers when the mode is inactive. */
36
- disabledAria: string;
37
- }
38
-
39
- /**
40
- * Sync a settings-menu toggle button's `aria-checked`, `aria-label`,
41
- * and visible text node with the current mode state. Both properties
42
- * are updated unconditionally so the element stays in sync even if a
43
- * previous render left them stale.
44
- *
45
- * The caller owns the DOM nodes; this helper only mutates known-safe
46
- * properties (no innerHTML), so a translated label with angle brackets
47
- * or ampersands is stored as text, not parsed as markup.
48
- */
49
- export function updateToggleMenuItem(
50
- button: HTMLElement | null | undefined,
51
- textElement: Element | null | undefined,
52
- state: ToggleMenuItemState
53
- ): void {
54
- if (!button) return;
55
-
56
- button.setAttribute('aria-checked', state.enabled ? 'true' : 'false');
57
- button.setAttribute('aria-label', state.enabled ? state.enabledAria : state.disabledAria);
58
-
59
- if (textElement) {
60
- // `textContent` exists on `Node`, so we can accept the broader
61
- // `Element` type here and let callers pass either an `HTMLElement`
62
- // (TranscriptManager keeps the tighter type for the sake of its
63
- // `.style` access elsewhere) or a plain `Element` returned by
64
- // `querySelector`.
65
- textElement.textContent = state.enabled ? state.enabledText : state.disabledText;
66
- }
67
- }
68
-
69
- export interface PositionSettingsMenuOptions {
70
- /**
71
- * Horizontal anchoring:
72
- * - `'left'` : align the menu's left edge with the button's left edge
73
- * (the TranscriptManager convention — simple panels).
74
- * - `'center'` : centre the menu horizontally on the button, with
75
- * viewport-edge clamping (the SignLanguageManager
76
- * convention — small floating panel that can end up
77
- * near the viewport edges).
78
- */
79
- align?: 'left' | 'center';
80
- /** Vertical gap (px) between button and menu. */
81
- gap?: number;
82
- /** Extra breathing room (px) before we decide to flip above. */
83
- spaceReserve?: number;
84
- }
85
-
86
- /**
87
- * Position a settings menu relative to its button, using offsets that
88
- * are relative to the button's offset parent (so transforms on ancestor
89
- * elements don't throw the layout off).
90
- *
91
- * When there isn't enough room below the button but there is room
92
- * above, the menu flips upward and picks up a `vidply-menu-above` CSS
93
- * class so arrows / drop-shadows can be mirrored via stylesheet.
94
- */
95
- export function positionSettingsMenu(
96
- menu: HTMLElement | null | undefined,
97
- button: HTMLElement | null | undefined,
98
- opts: PositionSettingsMenuOptions = {}
99
- ): void {
100
- if (!menu || !button) return;
101
- const { align = 'left', gap = 4, spaceReserve = 20 } = opts;
102
-
103
- const parentContainer = button.parentElement;
104
- if (!parentContainer) return;
105
-
106
- const buttonRect = button.getBoundingClientRect();
107
- const parentRect = parentContainer.getBoundingClientRect();
108
- const menuRect = menu.getBoundingClientRect();
109
- const viewportHeight = window.innerHeight;
110
- const viewportWidth = window.innerWidth;
111
-
112
- const buttonBottom = buttonRect.bottom - parentRect.top;
113
- const buttonTop = buttonRect.top - parentRect.top;
114
- const buttonLeftOffset = buttonRect.left - parentRect.left;
115
- const buttonCenterX = buttonLeftOffset + buttonRect.width / 2;
116
-
117
- const spaceAbove = buttonRect.top;
118
- const spaceBelow = viewportHeight - buttonRect.bottom;
119
-
120
- // Vertical placement: default below, flip above when tight.
121
- let menuTop: number | null = buttonBottom + gap;
122
- let menuBottomPx: number | null = null;
123
- if (spaceBelow < menuRect.height + spaceReserve && spaceAbove > spaceBelow) {
124
- if (align === 'center') {
125
- // Measure from the bottom edge of the parent so the menu sits
126
- // directly above the button without being pushed off-screen.
127
- const parentHeight = parentRect.bottom - parentRect.top;
128
- menuBottomPx = parentHeight - buttonTop + gap;
129
- menuTop = null;
130
- } else {
131
- menuTop = buttonTop - menuRect.height - gap;
132
- }
133
- menu.classList.add('vidply-menu-above');
134
- } else {
135
- menu.classList.remove('vidply-menu-above');
136
- }
137
-
138
- // Horizontal placement: simple left-align vs viewport-clamped centre.
139
- let leftValue: string;
140
- let rightValue: string;
141
- let transform: string;
142
-
143
- if (align === 'center') {
144
- const menuLeftAbsolute = buttonRect.left + buttonRect.width / 2 - menuRect.width / 2;
145
- if (menuLeftAbsolute < 10) {
146
- leftValue = '0';
147
- rightValue = 'auto';
148
- transform = 'translateX(0)';
149
- } else if (menuLeftAbsolute + menuRect.width > viewportWidth - 10) {
150
- leftValue = 'auto';
151
- rightValue = '0';
152
- transform = 'translateX(0)';
153
- } else {
154
- leftValue = `${buttonCenterX}px`;
155
- rightValue = 'auto';
156
- transform = 'translateX(-50%)';
157
- }
158
- } else {
159
- leftValue = `${buttonLeftOffset}px`;
160
- rightValue = 'auto';
161
- transform = 'translateX(0)';
162
- }
163
-
164
- if (menuTop !== null) {
165
- menu.style.top = `${menuTop}px`;
166
- menu.style.bottom = 'auto';
167
- } else if (menuBottomPx !== null) {
168
- menu.style.top = 'auto';
169
- menu.style.bottom = `${menuBottomPx}px`;
170
- }
171
-
172
- menu.style.left = leftValue;
173
- menu.style.right = rightValue;
174
- menu.style.transform = transform;
175
- }
176
-
177
- /**
178
- * RAF-deferred wrapper around {@link positionSettingsMenu}. Some
179
- * callers need to re-position after layout has settled (e.g. when the
180
- * panel size just changed and `getBoundingClientRect` would still
181
- * return the pre-change measurements on this frame).
182
- */
183
- export function positionSettingsMenuDeferred(
184
- menu: HTMLElement | null | undefined,
185
- button: HTMLElement | null | undefined,
186
- opts: PositionSettingsMenuOptions = {}
187
- ): void {
188
- requestAnimationFrame(() => {
189
- // The extra setTimeout is intentional: older WebKit still reports
190
- // stale geometry during the first RAF callback following a DOM
191
- // mutation. A 10ms trailing microtask gives layout a chance to
192
- // settle without being user-perceptible.
193
- setTimeout(() => positionSettingsMenu(menu, button, opts), 10);
194
- });
195
- }
1
+ /**
2
+ * Shared helpers for the settings-menu items that toggle drag / resize
3
+ * mode on a `DraggableResizable`-powered panel.
4
+ *
5
+ * Previously `SignLanguageManager` and `TranscriptManager` both carried
6
+ * near-identical copies of:
7
+ * - `updateDragOptionState` / `updateResizeOptionState`
8
+ * (button `aria-checked` + label + i18n-driven text sync)
9
+ * - `positionSettingsMenuImmediate`
10
+ * (place the menu relative to the settings button, with above/below
11
+ * flipping and viewport-edge clamping)
12
+ *
13
+ * This module owns those pieces so a future manager (or a refactor of
14
+ * an existing one) doesn't have to re-derive the rules from scratch.
15
+ *
16
+ * NOTE: the full "DraggablePanel" abstraction that also owns the menu
17
+ * *contents* (language selector, style dialog, pinned state, etc.) is
18
+ * deliberately NOT attempted here — the per-manager specifics are
19
+ * significant and an attempt to unify them would churn security-
20
+ * sensitive code without meaningfully reducing LOC.
21
+ */
22
+
23
+ /** Visual state of a two-state ("currently enabled / currently disabled")
24
+ * toggle menu item. */
25
+ export interface ToggleMenuItemState {
26
+ /** Whether the underlying mode is currently active. */
27
+ enabled: boolean;
28
+ /** Label shown when the mode is active (i.e. the label invites the
29
+ * user to disable it). */
30
+ enabledText: string;
31
+ /** Label shown when the mode is inactive. */
32
+ disabledText: string;
33
+ /** `aria-label` for screen readers when the mode is active. */
34
+ enabledAria: string;
35
+ /** `aria-label` for screen readers when the mode is inactive. */
36
+ disabledAria: string;
37
+ }
38
+
39
+ /**
40
+ * Sync a settings-menu toggle button's `aria-checked`, `aria-label`,
41
+ * and visible text node with the current mode state. Both properties
42
+ * are updated unconditionally so the element stays in sync even if a
43
+ * previous render left them stale.
44
+ *
45
+ * The caller owns the DOM nodes; this helper only mutates known-safe
46
+ * properties (no innerHTML), so a translated label with angle brackets
47
+ * or ampersands is stored as text, not parsed as markup.
48
+ */
49
+ export function updateToggleMenuItem(
50
+ button: HTMLElement | null | undefined,
51
+ textElement: Element | null | undefined,
52
+ state: ToggleMenuItemState
53
+ ): void {
54
+ if (!button) return;
55
+
56
+ button.setAttribute('aria-checked', state.enabled ? 'true' : 'false');
57
+ button.setAttribute('aria-label', state.enabled ? state.enabledAria : state.disabledAria);
58
+
59
+ if (textElement) {
60
+ // `textContent` exists on `Node`, so we can accept the broader
61
+ // `Element` type here and let callers pass either an `HTMLElement`
62
+ // (TranscriptManager keeps the tighter type for the sake of its
63
+ // `.style` access elsewhere) or a plain `Element` returned by
64
+ // `querySelector`.
65
+ textElement.textContent = state.enabled ? state.enabledText : state.disabledText;
66
+ }
67
+ }
68
+
69
+ export interface PositionSettingsMenuOptions {
70
+ /**
71
+ * Horizontal anchoring:
72
+ * - `'left'` : align the menu's left edge with the button's left edge
73
+ * (the TranscriptManager convention — simple panels).
74
+ * - `'center'` : centre the menu horizontally on the button, with
75
+ * viewport-edge clamping (the SignLanguageManager
76
+ * convention — small floating panel that can end up
77
+ * near the viewport edges).
78
+ */
79
+ align?: 'left' | 'center';
80
+ /** Vertical gap (px) between button and menu. */
81
+ gap?: number;
82
+ /** Extra breathing room (px) before we decide to flip above. */
83
+ spaceReserve?: number;
84
+ }
85
+
86
+ /**
87
+ * Position a settings menu relative to its button, using offsets that
88
+ * are relative to the button's offset parent (so transforms on ancestor
89
+ * elements don't throw the layout off).
90
+ *
91
+ * When there isn't enough room below the button but there is room
92
+ * above, the menu flips upward and picks up a `vidply-menu-above` CSS
93
+ * class so arrows / drop-shadows can be mirrored via stylesheet.
94
+ */
95
+ export function positionSettingsMenu(
96
+ menu: HTMLElement | null | undefined,
97
+ button: HTMLElement | null | undefined,
98
+ opts: PositionSettingsMenuOptions = {}
99
+ ): void {
100
+ if (!menu || !button) return;
101
+ const { align = 'left', gap = 4, spaceReserve = 20 } = opts;
102
+
103
+ const parentContainer = button.parentElement;
104
+ if (!parentContainer) return;
105
+
106
+ const buttonRect = button.getBoundingClientRect();
107
+ const parentRect = parentContainer.getBoundingClientRect();
108
+ const menuRect = menu.getBoundingClientRect();
109
+ const viewportHeight = window.innerHeight;
110
+ const viewportWidth = window.innerWidth;
111
+
112
+ const buttonBottom = buttonRect.bottom - parentRect.top;
113
+ const buttonTop = buttonRect.top - parentRect.top;
114
+ const buttonLeftOffset = buttonRect.left - parentRect.left;
115
+ const buttonCenterX = buttonLeftOffset + buttonRect.width / 2;
116
+
117
+ const spaceAbove = buttonRect.top;
118
+ const spaceBelow = viewportHeight - buttonRect.bottom;
119
+
120
+ // Vertical placement: default below, flip above when tight.
121
+ let menuTop: number | null = buttonBottom + gap;
122
+ let menuBottomPx: number | null = null;
123
+ if (spaceBelow < menuRect.height + spaceReserve && spaceAbove > spaceBelow) {
124
+ if (align === 'center') {
125
+ // Measure from the bottom edge of the parent so the menu sits
126
+ // directly above the button without being pushed off-screen.
127
+ const parentHeight = parentRect.bottom - parentRect.top;
128
+ menuBottomPx = parentHeight - buttonTop + gap;
129
+ menuTop = null;
130
+ } else {
131
+ menuTop = buttonTop - menuRect.height - gap;
132
+ }
133
+ menu.classList.add('vidply-menu-above');
134
+ } else {
135
+ menu.classList.remove('vidply-menu-above');
136
+ }
137
+
138
+ // Horizontal placement: simple left-align vs viewport-clamped centre.
139
+ let leftValue: string;
140
+ let rightValue: string;
141
+ let transform: string;
142
+
143
+ if (align === 'center') {
144
+ const menuLeftAbsolute = buttonRect.left + buttonRect.width / 2 - menuRect.width / 2;
145
+ if (menuLeftAbsolute < 10) {
146
+ leftValue = '0';
147
+ rightValue = 'auto';
148
+ transform = 'translateX(0)';
149
+ } else if (menuLeftAbsolute + menuRect.width > viewportWidth - 10) {
150
+ leftValue = 'auto';
151
+ rightValue = '0';
152
+ transform = 'translateX(0)';
153
+ } else {
154
+ leftValue = `${buttonCenterX}px`;
155
+ rightValue = 'auto';
156
+ transform = 'translateX(-50%)';
157
+ }
158
+ } else {
159
+ leftValue = `${buttonLeftOffset}px`;
160
+ rightValue = 'auto';
161
+ transform = 'translateX(0)';
162
+ }
163
+
164
+ if (menuTop !== null) {
165
+ menu.style.top = `${menuTop}px`;
166
+ menu.style.bottom = 'auto';
167
+ } else if (menuBottomPx !== null) {
168
+ menu.style.top = 'auto';
169
+ menu.style.bottom = `${menuBottomPx}px`;
170
+ }
171
+
172
+ menu.style.left = leftValue;
173
+ menu.style.right = rightValue;
174
+ menu.style.transform = transform;
175
+ }
176
+
177
+ /**
178
+ * RAF-deferred wrapper around {@link positionSettingsMenu}. Some
179
+ * callers need to re-position after layout has settled (e.g. when the
180
+ * panel size just changed and `getBoundingClientRect` would still
181
+ * return the pre-change measurements on this frame).
182
+ */
183
+ export function positionSettingsMenuDeferred(
184
+ menu: HTMLElement | null | undefined,
185
+ button: HTMLElement | null | undefined,
186
+ opts: PositionSettingsMenuOptions = {}
187
+ ): void {
188
+ requestAnimationFrame(() => {
189
+ // The extra setTimeout is intentional: older WebKit still reports
190
+ // stale geometry during the first RAF callback following a DOM
191
+ // mutation. A 10ms trailing microtask gives layout a chance to
192
+ // settle without being user-perceptible.
193
+ setTimeout(() => positionSettingsMenu(menu, button, opts), 10);
194
+ });
195
+ }