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,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
+ }