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,539 +1,539 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Owns the settings-menu lifecycle for a `DraggableResizable`-powered
|
|
3
|
-
* floating panel: create, show, hide, outside-click dismissal, keyboard
|
|
4
|
-
* navigation, viewport-aware positioning, and the drag-mode / resize-
|
|
5
|
-
* mode toggle items.
|
|
6
|
-
*
|
|
7
|
-
* `SignLanguageManager` and `TranscriptManager` both present the same
|
|
8
|
-
* pattern: a settings button that opens a menu containing a "Keyboard
|
|
9
|
-
* drag mode" toggle, a "Resize mode" toggle, manager-specific extras,
|
|
10
|
-
* and a close item. This class captures that pattern so each manager
|
|
11
|
-
* only has to describe:
|
|
12
|
-
* - how to resolve its `DraggableResizable` (for state readback),
|
|
13
|
-
* - which i18n keys to use on the toggle items,
|
|
14
|
-
* - where to place the menu in its own layout (via `getMenuParent`),
|
|
15
|
-
* - how to toggle each mode (`onDragItemClick` / `onResizeItemClick`
|
|
16
|
-
* run the manager's side effects — announcements, badges, focus),
|
|
17
|
-
* - any extra menu items (`buildExtraItems`, e.g. style dialog,
|
|
18
|
-
* timestamp toggle for TranscriptManager).
|
|
19
|
-
*
|
|
20
|
-
* The manager's own public API (`showSettingsMenu`, `toggleKeyboardDragMode`,
|
|
21
|
-
* `hideSettingsMenu`, ...) is preserved by forwarding to this panel.
|
|
22
|
-
* External code that reads `settingsMenuVisible` or the option-button
|
|
23
|
-
* elements keeps working via getters on the manager that proxy the
|
|
24
|
-
* panel's state.
|
|
25
|
-
*
|
|
26
|
-
* Event listeners attached to `document` are tied to the player's
|
|
27
|
-
* `lifecycleSignal` so they are torn down automatically when the
|
|
28
|
-
* player is destroyed.
|
|
29
|
-
*/
|
|
30
|
-
|
|
31
|
-
import { DOMUtils } from './DOMUtils.js';
|
|
32
|
-
import { i18n } from '../i18n/i18n.js';
|
|
33
|
-
import {
|
|
34
|
-
attachMenuKeyboardNavigation,
|
|
35
|
-
createMenuItem,
|
|
36
|
-
focusFirstMenuItem,
|
|
37
|
-
} from './MenuUtils.js';
|
|
38
|
-
import {
|
|
39
|
-
positionSettingsMenu,
|
|
40
|
-
positionSettingsMenuDeferred,
|
|
41
|
-
updateToggleMenuItem,
|
|
42
|
-
} from './DraggablePanelMenu.js';
|
|
43
|
-
|
|
44
|
-
import type { Player } from '../core/Player.js';
|
|
45
|
-
import type { DraggableResizable } from './DraggableResizable.js';
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* i18n keys used for the drag-mode and resize-mode toggle items and
|
|
49
|
-
* the "Close" item. All values must be translation keys (passed
|
|
50
|
-
* through `i18n.t()` by `createMenuItem`/`updateToggleMenuItem`).
|
|
51
|
-
*/
|
|
52
|
-
export interface DraggablePanelI18nKeys {
|
|
53
|
-
enableDrag: string;
|
|
54
|
-
disableDrag: string;
|
|
55
|
-
enableDragAria: string;
|
|
56
|
-
disableDragAria: string;
|
|
57
|
-
enableResize: string;
|
|
58
|
-
disableResize: string;
|
|
59
|
-
enableResizeAria: string;
|
|
60
|
-
disableResizeAria: string;
|
|
61
|
-
closeMenu: string;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
export interface DraggablePanelExtrasContext {
|
|
65
|
-
menu: HTMLElement;
|
|
66
|
-
itemClass: string;
|
|
67
|
-
classPrefix: string;
|
|
68
|
-
/**
|
|
69
|
-
* Strip the tooltip and duplicate `button-text` nodes from a menu
|
|
70
|
-
* item. `createMenuItem` is shared with toolbar buttons that want a
|
|
71
|
-
* hover tooltip; settings-menu rows already show the same text
|
|
72
|
-
* inline, so the tooltip would cause screen readers to read the
|
|
73
|
-
* label twice. Exposed here so manager-provided extras can reuse
|
|
74
|
-
* the same stripping logic the panel applies to its own items.
|
|
75
|
-
*/
|
|
76
|
-
stripInlineTooltip: (item: HTMLElement) => void;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
export interface DraggablePanelOptions {
|
|
80
|
-
player: Player;
|
|
81
|
-
/**
|
|
82
|
-
* Namespace for CSS classes, e.g. `'sign-language'` or `'transcript'`.
|
|
83
|
-
* The final menu class is `{classPrefix}-{namespace}-settings-menu`
|
|
84
|
-
* and item class is `{classPrefix}-{namespace}-settings-item`.
|
|
85
|
-
*/
|
|
86
|
-
namespace: string;
|
|
87
|
-
/** The button that opens the menu. */
|
|
88
|
-
settingsButton: HTMLElement;
|
|
89
|
-
/** Lazy draggable lookup (it may be re-created by the manager). */
|
|
90
|
-
getDraggable: () => DraggableResizable | null;
|
|
91
|
-
i18nKeys: DraggablePanelI18nKeys;
|
|
92
|
-
/**
|
|
93
|
-
* Horizontal anchoring of the menu relative to the settings button.
|
|
94
|
-
* - `'left'` : align left edge (Transcript — header-row button).
|
|
95
|
-
* - `'center'` : centre horizontally with viewport clamping (Sign
|
|
96
|
-
* language — floating near viewport edges).
|
|
97
|
-
*/
|
|
98
|
-
menuAlign: 'left' | 'center';
|
|
99
|
-
/** Where to attach the menu node when first created. */
|
|
100
|
-
getMenuParent: () => HTMLElement | null;
|
|
101
|
-
/**
|
|
102
|
-
* Called when the drag-mode item is clicked. The manager is
|
|
103
|
-
* expected to toggle drag mode and run any side effects
|
|
104
|
-
* (announcements, badges, focus). After this runs the panel will
|
|
105
|
-
* refresh menu item state.
|
|
106
|
-
*/
|
|
107
|
-
onDragItemClick: (panel: DraggablePanel) => void;
|
|
108
|
-
/** Same for the resize-mode item. */
|
|
109
|
-
onResizeItemClick: (panel: DraggablePanel) => void;
|
|
110
|
-
/**
|
|
111
|
-
* Insert manager-specific items (style dialog, timestamps, ...)
|
|
112
|
-
* between the resize item and the close item.
|
|
113
|
-
*/
|
|
114
|
-
buildExtraItems?: (ctx: DraggablePanelExtrasContext) => void;
|
|
115
|
-
/** Optional: vertical gap (px) between button and menu. */
|
|
116
|
-
menuGap?: number;
|
|
117
|
-
/** Optional: breathing room (px) before the menu flips above. */
|
|
118
|
-
menuSpaceReserve?: number;
|
|
119
|
-
/**
|
|
120
|
-
* Where to attach the mode-feedback badge while keyboard drag /
|
|
121
|
-
* pointer resize mode is active. If omitted, {@link DraggablePanel.showBadge}
|
|
122
|
-
* is a no-op (the manager effectively opts out of the badge).
|
|
123
|
-
*/
|
|
124
|
-
getBadgeHost?: () => HTMLElement | null;
|
|
125
|
-
/**
|
|
126
|
-
* CSS class for the badge element. Defaults to
|
|
127
|
-
* `{classPrefix}-{namespace}-mode-badge`. Sign-language passes a
|
|
128
|
-
* shorter class (`vidply-sign-mode-badge`) to preserve existing
|
|
129
|
-
* CSS. Transcript uses the default.
|
|
130
|
-
*/
|
|
131
|
-
badgeClass?: string;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
export class DraggablePanel {
|
|
135
|
-
private readonly opts: DraggablePanelOptions;
|
|
136
|
-
|
|
137
|
-
/** Populated lazily on first `show()`. */
|
|
138
|
-
settingsMenu: HTMLElement | null = null;
|
|
139
|
-
settingsMenuVisible = false;
|
|
140
|
-
dragOptionButton: HTMLElement | null = null;
|
|
141
|
-
dragOptionText: Element | null = null;
|
|
142
|
-
resizeOptionButton: HTMLElement | null = null;
|
|
143
|
-
resizeOptionText: Element | null = null;
|
|
144
|
-
|
|
145
|
-
private _justOpened = false;
|
|
146
|
-
private _justOpenedTimer: ReturnType<typeof setTimeout> | null = null;
|
|
147
|
-
private _keyHandler: ((e: KeyboardEvent) => void) | null = null;
|
|
148
|
-
private _documentClick: ((e: MouseEvent) => void) | null = null;
|
|
149
|
-
private _documentClickAdded = false;
|
|
150
|
-
private _modeBadge: HTMLElement | null = null;
|
|
151
|
-
|
|
152
|
-
constructor(opts: DraggablePanelOptions) {
|
|
153
|
-
this.opts = opts;
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
/** True while the just-opened debounce window (prevents the same
|
|
157
|
-
* click that opened the menu from also closing it via document
|
|
158
|
-
* `mousedown` / `click`). */
|
|
159
|
-
get justOpened(): boolean {
|
|
160
|
-
return this._justOpened;
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
get classPrefix(): string {
|
|
164
|
-
return this.opts.player.options.classPrefix;
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
get menuClass(): string {
|
|
168
|
-
return `${this.classPrefix}-${this.opts.namespace}-settings-menu`;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
get itemClass(): string {
|
|
172
|
-
return `${this.classPrefix}-${this.opts.namespace}-settings-item`;
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
/**
|
|
176
|
-
* Show the menu. First call creates the DOM; subsequent calls reuse
|
|
177
|
-
* it. Refreshes menu item state from the current draggable.
|
|
178
|
-
*/
|
|
179
|
-
show(): void {
|
|
180
|
-
this._markJustOpened(350);
|
|
181
|
-
this._ensureDocumentClickHandler();
|
|
182
|
-
|
|
183
|
-
if (this.settingsMenu) {
|
|
184
|
-
this.settingsMenu.style.display = 'block';
|
|
185
|
-
this.settingsMenuVisible = true;
|
|
186
|
-
this.opts.settingsButton.setAttribute('aria-expanded', 'true');
|
|
187
|
-
this._attachKeyboardNavigation();
|
|
188
|
-
this._positionImmediate();
|
|
189
|
-
this.refreshState();
|
|
190
|
-
focusFirstMenuItem(this.settingsMenu, `.${this.itemClass}`);
|
|
191
|
-
return;
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
this._createMenu();
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
/**
|
|
198
|
-
* Hide the menu. By default returns focus to the settings button;
|
|
199
|
-
* callers can opt out when the next interaction should land
|
|
200
|
-
* elsewhere (e.g. on the wrapper after enabling drag mode).
|
|
201
|
-
*/
|
|
202
|
-
hide({ focusButton = true }: { focusButton?: boolean } = {}): void {
|
|
203
|
-
if (!this.settingsMenu) return;
|
|
204
|
-
|
|
205
|
-
this.settingsMenu.style.display = 'none';
|
|
206
|
-
this.settingsMenuVisible = false;
|
|
207
|
-
this._clearJustOpened();
|
|
208
|
-
|
|
209
|
-
if (this._keyHandler) {
|
|
210
|
-
this.settingsMenu.removeEventListener('keydown', this._keyHandler, true);
|
|
211
|
-
this._keyHandler = null;
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
const items = this.settingsMenu.querySelectorAll<HTMLElement>(`.${this.itemClass}`);
|
|
215
|
-
items.forEach((item) => item.setAttribute('tabindex', '-1'));
|
|
216
|
-
|
|
217
|
-
const { settingsButton } = this.opts;
|
|
218
|
-
settingsButton.setAttribute('aria-expanded', 'false');
|
|
219
|
-
if (focusButton) {
|
|
220
|
-
settingsButton.focus({ preventScroll: true });
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
toggle(): void {
|
|
225
|
-
if (this.settingsMenuVisible) {
|
|
226
|
-
this.hide();
|
|
227
|
-
} else {
|
|
228
|
-
this.show();
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
/** Set a short "just opened" guard so the document-click handler
|
|
233
|
-
* attached for outside-dismissal ignores the originating click. */
|
|
234
|
-
markJustOpenedForClick(): void {
|
|
235
|
-
if (this._documentClick) {
|
|
236
|
-
this._markJustOpened(100);
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
/** Refresh the drag and resize toggle item state from the draggable. */
|
|
241
|
-
refreshState(): void {
|
|
242
|
-
this.refreshDragState();
|
|
243
|
-
this.refreshResizeState();
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
refreshDragState(): void {
|
|
247
|
-
const draggable = this.opts.getDraggable();
|
|
248
|
-
updateToggleMenuItem(this.dragOptionButton, this.dragOptionText, {
|
|
249
|
-
enabled: Boolean(draggable?.keyboardDragMode),
|
|
250
|
-
enabledText: i18n.t(this.opts.i18nKeys.disableDrag),
|
|
251
|
-
disabledText: i18n.t(this.opts.i18nKeys.enableDrag),
|
|
252
|
-
enabledAria: i18n.t(this.opts.i18nKeys.disableDragAria),
|
|
253
|
-
disabledAria: i18n.t(this.opts.i18nKeys.enableDragAria),
|
|
254
|
-
});
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
refreshResizeState(): void {
|
|
258
|
-
const draggable = this.opts.getDraggable();
|
|
259
|
-
updateToggleMenuItem(this.resizeOptionButton, this.resizeOptionText, {
|
|
260
|
-
enabled: Boolean(draggable?.pointerResizeMode),
|
|
261
|
-
enabledText: i18n.t(this.opts.i18nKeys.disableResize),
|
|
262
|
-
disabledText: i18n.t(this.opts.i18nKeys.enableResize),
|
|
263
|
-
enabledAria: i18n.t(this.opts.i18nKeys.disableResizeAria),
|
|
264
|
-
disabledAria: i18n.t(this.opts.i18nKeys.enableResizeAria),
|
|
265
|
-
});
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
/**
|
|
269
|
-
* Show a persistent mode-feedback badge (e.g. "Drag mode: arrow
|
|
270
|
-
* keys to move, Esc to exit") anchored to the host element
|
|
271
|
-
* returned by `getBadgeHost`. Replaces any previous badge. The
|
|
272
|
-
* badge is a real DOM element (not a CSS pseudo-element) so its
|
|
273
|
-
* text is translatable, selectable, visible under high-contrast
|
|
274
|
-
* themes, and reflected in browser translation overlays.
|
|
275
|
-
*
|
|
276
|
-
* Marked `aria-hidden` because the accompanying live-region
|
|
277
|
-
* announcement (the manager's responsibility) already conveys the
|
|
278
|
-
* state change to assistive tech.
|
|
279
|
-
*/
|
|
280
|
-
showBadge(text: string): void {
|
|
281
|
-
const host = this.opts.getBadgeHost?.();
|
|
282
|
-
if (!host) return;
|
|
283
|
-
this.hideBadge();
|
|
284
|
-
|
|
285
|
-
const className =
|
|
286
|
-
this.opts.badgeClass ??
|
|
287
|
-
`${this.classPrefix}-${this.opts.namespace}-mode-badge`;
|
|
288
|
-
const badge = DOMUtils.createElement('span', {
|
|
289
|
-
className,
|
|
290
|
-
textContent: text,
|
|
291
|
-
attributes: { 'aria-hidden': 'true' },
|
|
292
|
-
});
|
|
293
|
-
host.appendChild(badge);
|
|
294
|
-
this._modeBadge = badge;
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
/** Remove the mode-feedback badge if one is showing. */
|
|
298
|
-
hideBadge(): void {
|
|
299
|
-
if (this._modeBadge && this._modeBadge.parentNode) {
|
|
300
|
-
this._modeBadge.remove();
|
|
301
|
-
}
|
|
302
|
-
this._modeBadge = null;
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
/** RAF-deferred reposition (e.g. after a panel resize). */
|
|
306
|
-
reposition(): void {
|
|
307
|
-
positionSettingsMenuDeferred(this.settingsMenu, this.opts.settingsButton, {
|
|
308
|
-
align: this.opts.menuAlign,
|
|
309
|
-
gap: this.opts.menuGap ?? 4,
|
|
310
|
-
spaceReserve: this.opts.menuSpaceReserve ?? 20,
|
|
311
|
-
});
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
/**
|
|
315
|
-
* Tear down any DOM/listeners owned by this panel. Safe to call
|
|
316
|
-
* multiple times. Callers must still drop their own references.
|
|
317
|
-
*/
|
|
318
|
-
destroy(): void {
|
|
319
|
-
if (this._justOpenedTimer) {
|
|
320
|
-
clearTimeout(this._justOpenedTimer);
|
|
321
|
-
this._justOpenedTimer = null;
|
|
322
|
-
}
|
|
323
|
-
this._justOpened = false;
|
|
324
|
-
|
|
325
|
-
this.hideBadge();
|
|
326
|
-
|
|
327
|
-
if (this.settingsMenu) {
|
|
328
|
-
if (this._keyHandler) {
|
|
329
|
-
this.settingsMenu.removeEventListener('keydown', this._keyHandler, true);
|
|
330
|
-
}
|
|
331
|
-
this.settingsMenu.remove();
|
|
332
|
-
this.settingsMenu = null;
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
this._keyHandler = null;
|
|
336
|
-
this.settingsMenuVisible = false;
|
|
337
|
-
this.dragOptionButton = null;
|
|
338
|
-
this.dragOptionText = null;
|
|
339
|
-
this.resizeOptionButton = null;
|
|
340
|
-
this.resizeOptionText = null;
|
|
341
|
-
|
|
342
|
-
// The document-click listener was attached with the player's
|
|
343
|
-
// lifecycleSignal, so it is cleaned up automatically. We clear
|
|
344
|
-
// the local reference so a later show() re-installs.
|
|
345
|
-
this._documentClick = null;
|
|
346
|
-
this._documentClickAdded = false;
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
private _createMenu(): void {
|
|
350
|
-
const { player, settingsButton, i18nKeys } = this.opts;
|
|
351
|
-
|
|
352
|
-
const menu = DOMUtils.createElement('div', {
|
|
353
|
-
className: this.menuClass,
|
|
354
|
-
attributes: { role: 'menu' },
|
|
355
|
-
});
|
|
356
|
-
this.settingsMenu = menu;
|
|
357
|
-
|
|
358
|
-
const dragOption = createMenuItem({
|
|
359
|
-
classPrefix: this.classPrefix,
|
|
360
|
-
itemClass: this.itemClass,
|
|
361
|
-
icon: 'move',
|
|
362
|
-
label: i18nKeys.enableDrag,
|
|
363
|
-
hasTextClass: true,
|
|
364
|
-
onClick: (event) => {
|
|
365
|
-
event.preventDefault();
|
|
366
|
-
event.stopPropagation();
|
|
367
|
-
this.opts.onDragItemClick(this);
|
|
368
|
-
this.refreshState();
|
|
369
|
-
},
|
|
370
|
-
});
|
|
371
|
-
dragOption.setAttribute('role', 'switch');
|
|
372
|
-
dragOption.setAttribute('aria-checked', 'false');
|
|
373
|
-
dragOption.setAttribute('data-setting', 'keyboard-drag');
|
|
374
|
-
this._stripInlineTooltip(dragOption);
|
|
375
|
-
this.dragOptionButton = dragOption;
|
|
376
|
-
this.dragOptionText = dragOption.querySelector(`.${this.classPrefix}-settings-text`);
|
|
377
|
-
|
|
378
|
-
const resizeOption = createMenuItem({
|
|
379
|
-
classPrefix: this.classPrefix,
|
|
380
|
-
itemClass: this.itemClass,
|
|
381
|
-
icon: 'resize',
|
|
382
|
-
label: i18nKeys.enableResize,
|
|
383
|
-
hasTextClass: true,
|
|
384
|
-
onClick: (event) => {
|
|
385
|
-
event.preventDefault();
|
|
386
|
-
event.stopPropagation();
|
|
387
|
-
this.opts.onResizeItemClick(this);
|
|
388
|
-
this.refreshState();
|
|
389
|
-
},
|
|
390
|
-
});
|
|
391
|
-
resizeOption.setAttribute('role', 'switch');
|
|
392
|
-
resizeOption.setAttribute('aria-checked', 'false');
|
|
393
|
-
this._stripInlineTooltip(resizeOption);
|
|
394
|
-
this.resizeOptionButton = resizeOption;
|
|
395
|
-
this.resizeOptionText = resizeOption.querySelector(`.${this.classPrefix}-settings-text`);
|
|
396
|
-
|
|
397
|
-
menu.appendChild(dragOption);
|
|
398
|
-
menu.appendChild(resizeOption);
|
|
399
|
-
|
|
400
|
-
if (this.opts.buildExtraItems) {
|
|
401
|
-
this.opts.buildExtraItems({
|
|
402
|
-
menu,
|
|
403
|
-
itemClass: this.itemClass,
|
|
404
|
-
classPrefix: this.classPrefix,
|
|
405
|
-
stripInlineTooltip: (item) => this._stripInlineTooltip(item),
|
|
406
|
-
});
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
const closeOption = createMenuItem({
|
|
410
|
-
classPrefix: this.classPrefix,
|
|
411
|
-
itemClass: this.itemClass,
|
|
412
|
-
icon: 'close',
|
|
413
|
-
label: i18nKeys.closeMenu,
|
|
414
|
-
onClick: () => this.hide(),
|
|
415
|
-
});
|
|
416
|
-
this._stripInlineTooltip(closeOption);
|
|
417
|
-
menu.appendChild(closeOption);
|
|
418
|
-
|
|
419
|
-
menu.style.visibility = 'hidden';
|
|
420
|
-
menu.style.display = 'block';
|
|
421
|
-
|
|
422
|
-
const parent = this.opts.getMenuParent();
|
|
423
|
-
if (settingsButton.parentNode) {
|
|
424
|
-
settingsButton.insertAdjacentElement('afterend', menu);
|
|
425
|
-
} else if (parent) {
|
|
426
|
-
parent.appendChild(menu);
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
this._positionImmediate();
|
|
430
|
-
|
|
431
|
-
requestAnimationFrame(() => {
|
|
432
|
-
if (this.settingsMenu) {
|
|
433
|
-
this.settingsMenu.style.visibility = 'visible';
|
|
434
|
-
}
|
|
435
|
-
});
|
|
436
|
-
|
|
437
|
-
this._attachKeyboardNavigation();
|
|
438
|
-
|
|
439
|
-
this.settingsMenuVisible = true;
|
|
440
|
-
settingsButton.setAttribute('aria-expanded', 'true');
|
|
441
|
-
this.refreshState();
|
|
442
|
-
|
|
443
|
-
focusFirstMenuItem(menu, `.${this.itemClass}`);
|
|
444
|
-
|
|
445
|
-
// Don't reveal the fact that we created the menu to callers that
|
|
446
|
-
// didn't trigger show() explicitly — but this is always driven
|
|
447
|
-
// by a show() so the `player` reference is just for
|
|
448
|
-
// documentation clarity here.
|
|
449
|
-
void player;
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
private _attachKeyboardNavigation(): void {
|
|
453
|
-
const menu = this.settingsMenu;
|
|
454
|
-
if (!menu) return;
|
|
455
|
-
if (this._keyHandler) {
|
|
456
|
-
menu.removeEventListener('keydown', this._keyHandler, true);
|
|
457
|
-
}
|
|
458
|
-
const handler = attachMenuKeyboardNavigation(
|
|
459
|
-
menu,
|
|
460
|
-
this.opts.settingsButton,
|
|
461
|
-
`.${this.itemClass}`,
|
|
462
|
-
() => this.hide({ focusButton: true })
|
|
463
|
-
);
|
|
464
|
-
this._keyHandler = handler ?? null;
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
private _positionImmediate(): void {
|
|
468
|
-
positionSettingsMenu(this.settingsMenu, this.opts.settingsButton, {
|
|
469
|
-
align: this.opts.menuAlign,
|
|
470
|
-
gap: this.opts.menuGap ?? 4,
|
|
471
|
-
spaceReserve: this.opts.menuSpaceReserve ?? 20,
|
|
472
|
-
});
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
/**
|
|
476
|
-
* Remove tooltip and duplicate button-text nodes from a menu item.
|
|
477
|
-
* `createMenuItem` is used both for toolbar buttons (which want a
|
|
478
|
-
* tooltip) and for settings-menu rows (which show the same text
|
|
479
|
-
* inline). This strips the duplicated pieces so screen readers
|
|
480
|
-
* don't read the label twice.
|
|
481
|
-
*/
|
|
482
|
-
private _stripInlineTooltip(item: HTMLElement): void {
|
|
483
|
-
const tooltip = item.querySelector(`.${this.classPrefix}-tooltip`);
|
|
484
|
-
if (tooltip) tooltip.remove();
|
|
485
|
-
const buttonText = item.querySelector(`.${this.classPrefix}-button-text`);
|
|
486
|
-
if (buttonText) buttonText.remove();
|
|
487
|
-
}
|
|
488
|
-
|
|
489
|
-
private _markJustOpened(durationMs: number): void {
|
|
490
|
-
this._justOpened = true;
|
|
491
|
-
if (this._justOpenedTimer) {
|
|
492
|
-
clearTimeout(this._justOpenedTimer);
|
|
493
|
-
}
|
|
494
|
-
this._justOpenedTimer = setTimeout(() => {
|
|
495
|
-
this._justOpened = false;
|
|
496
|
-
this._justOpenedTimer = null;
|
|
497
|
-
}, durationMs);
|
|
498
|
-
}
|
|
499
|
-
|
|
500
|
-
private _clearJustOpened(): void {
|
|
501
|
-
this._justOpened = false;
|
|
502
|
-
if (this._justOpenedTimer) {
|
|
503
|
-
clearTimeout(this._justOpenedTimer);
|
|
504
|
-
this._justOpenedTimer = null;
|
|
505
|
-
}
|
|
506
|
-
}
|
|
507
|
-
|
|
508
|
-
private _ensureDocumentClickHandler(): void {
|
|
509
|
-
if (this._documentClickAdded) return;
|
|
510
|
-
|
|
511
|
-
this._documentClick = (event: MouseEvent) => {
|
|
512
|
-
if (this._justOpened) return;
|
|
513
|
-
const target = event.target as Node | null;
|
|
514
|
-
const { settingsButton } = this.opts;
|
|
515
|
-
|
|
516
|
-
if (settingsButton === target || (target && settingsButton.contains(target))) {
|
|
517
|
-
return;
|
|
518
|
-
}
|
|
519
|
-
if (this.settingsMenu && target && this.settingsMenu.contains(target)) {
|
|
520
|
-
return;
|
|
521
|
-
}
|
|
522
|
-
if (this.settingsMenuVisible) {
|
|
523
|
-
this.hide();
|
|
524
|
-
}
|
|
525
|
-
};
|
|
526
|
-
|
|
527
|
-
// Delay attach so the click that opened the menu doesn't instantly
|
|
528
|
-
// close it even if the justOpened flag somehow races.
|
|
529
|
-
setTimeout(() => {
|
|
530
|
-
const handler = this._documentClick;
|
|
531
|
-
if (!handler) return;
|
|
532
|
-
document.addEventListener('mousedown', handler, {
|
|
533
|
-
capture: true,
|
|
534
|
-
signal: this.opts.player.lifecycleSignal,
|
|
535
|
-
});
|
|
536
|
-
this._documentClickAdded = true;
|
|
537
|
-
}, 300);
|
|
538
|
-
}
|
|
539
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Owns the settings-menu lifecycle for a `DraggableResizable`-powered
|
|
3
|
+
* floating panel: create, show, hide, outside-click dismissal, keyboard
|
|
4
|
+
* navigation, viewport-aware positioning, and the drag-mode / resize-
|
|
5
|
+
* mode toggle items.
|
|
6
|
+
*
|
|
7
|
+
* `SignLanguageManager` and `TranscriptManager` both present the same
|
|
8
|
+
* pattern: a settings button that opens a menu containing a "Keyboard
|
|
9
|
+
* drag mode" toggle, a "Resize mode" toggle, manager-specific extras,
|
|
10
|
+
* and a close item. This class captures that pattern so each manager
|
|
11
|
+
* only has to describe:
|
|
12
|
+
* - how to resolve its `DraggableResizable` (for state readback),
|
|
13
|
+
* - which i18n keys to use on the toggle items,
|
|
14
|
+
* - where to place the menu in its own layout (via `getMenuParent`),
|
|
15
|
+
* - how to toggle each mode (`onDragItemClick` / `onResizeItemClick`
|
|
16
|
+
* run the manager's side effects — announcements, badges, focus),
|
|
17
|
+
* - any extra menu items (`buildExtraItems`, e.g. style dialog,
|
|
18
|
+
* timestamp toggle for TranscriptManager).
|
|
19
|
+
*
|
|
20
|
+
* The manager's own public API (`showSettingsMenu`, `toggleKeyboardDragMode`,
|
|
21
|
+
* `hideSettingsMenu`, ...) is preserved by forwarding to this panel.
|
|
22
|
+
* External code that reads `settingsMenuVisible` or the option-button
|
|
23
|
+
* elements keeps working via getters on the manager that proxy the
|
|
24
|
+
* panel's state.
|
|
25
|
+
*
|
|
26
|
+
* Event listeners attached to `document` are tied to the player's
|
|
27
|
+
* `lifecycleSignal` so they are torn down automatically when the
|
|
28
|
+
* player is destroyed.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
import { DOMUtils } from './DOMUtils.js';
|
|
32
|
+
import { i18n } from '../i18n/i18n.js';
|
|
33
|
+
import {
|
|
34
|
+
attachMenuKeyboardNavigation,
|
|
35
|
+
createMenuItem,
|
|
36
|
+
focusFirstMenuItem,
|
|
37
|
+
} from './MenuUtils.js';
|
|
38
|
+
import {
|
|
39
|
+
positionSettingsMenu,
|
|
40
|
+
positionSettingsMenuDeferred,
|
|
41
|
+
updateToggleMenuItem,
|
|
42
|
+
} from './DraggablePanelMenu.js';
|
|
43
|
+
|
|
44
|
+
import type { Player } from '../core/Player.js';
|
|
45
|
+
import type { DraggableResizable } from './DraggableResizable.js';
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* i18n keys used for the drag-mode and resize-mode toggle items and
|
|
49
|
+
* the "Close" item. All values must be translation keys (passed
|
|
50
|
+
* through `i18n.t()` by `createMenuItem`/`updateToggleMenuItem`).
|
|
51
|
+
*/
|
|
52
|
+
export interface DraggablePanelI18nKeys {
|
|
53
|
+
enableDrag: string;
|
|
54
|
+
disableDrag: string;
|
|
55
|
+
enableDragAria: string;
|
|
56
|
+
disableDragAria: string;
|
|
57
|
+
enableResize: string;
|
|
58
|
+
disableResize: string;
|
|
59
|
+
enableResizeAria: string;
|
|
60
|
+
disableResizeAria: string;
|
|
61
|
+
closeMenu: string;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface DraggablePanelExtrasContext {
|
|
65
|
+
menu: HTMLElement;
|
|
66
|
+
itemClass: string;
|
|
67
|
+
classPrefix: string;
|
|
68
|
+
/**
|
|
69
|
+
* Strip the tooltip and duplicate `button-text` nodes from a menu
|
|
70
|
+
* item. `createMenuItem` is shared with toolbar buttons that want a
|
|
71
|
+
* hover tooltip; settings-menu rows already show the same text
|
|
72
|
+
* inline, so the tooltip would cause screen readers to read the
|
|
73
|
+
* label twice. Exposed here so manager-provided extras can reuse
|
|
74
|
+
* the same stripping logic the panel applies to its own items.
|
|
75
|
+
*/
|
|
76
|
+
stripInlineTooltip: (item: HTMLElement) => void;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export interface DraggablePanelOptions {
|
|
80
|
+
player: Player;
|
|
81
|
+
/**
|
|
82
|
+
* Namespace for CSS classes, e.g. `'sign-language'` or `'transcript'`.
|
|
83
|
+
* The final menu class is `{classPrefix}-{namespace}-settings-menu`
|
|
84
|
+
* and item class is `{classPrefix}-{namespace}-settings-item`.
|
|
85
|
+
*/
|
|
86
|
+
namespace: string;
|
|
87
|
+
/** The button that opens the menu. */
|
|
88
|
+
settingsButton: HTMLElement;
|
|
89
|
+
/** Lazy draggable lookup (it may be re-created by the manager). */
|
|
90
|
+
getDraggable: () => DraggableResizable | null;
|
|
91
|
+
i18nKeys: DraggablePanelI18nKeys;
|
|
92
|
+
/**
|
|
93
|
+
* Horizontal anchoring of the menu relative to the settings button.
|
|
94
|
+
* - `'left'` : align left edge (Transcript — header-row button).
|
|
95
|
+
* - `'center'` : centre horizontally with viewport clamping (Sign
|
|
96
|
+
* language — floating near viewport edges).
|
|
97
|
+
*/
|
|
98
|
+
menuAlign: 'left' | 'center';
|
|
99
|
+
/** Where to attach the menu node when first created. */
|
|
100
|
+
getMenuParent: () => HTMLElement | null;
|
|
101
|
+
/**
|
|
102
|
+
* Called when the drag-mode item is clicked. The manager is
|
|
103
|
+
* expected to toggle drag mode and run any side effects
|
|
104
|
+
* (announcements, badges, focus). After this runs the panel will
|
|
105
|
+
* refresh menu item state.
|
|
106
|
+
*/
|
|
107
|
+
onDragItemClick: (panel: DraggablePanel) => void;
|
|
108
|
+
/** Same for the resize-mode item. */
|
|
109
|
+
onResizeItemClick: (panel: DraggablePanel) => void;
|
|
110
|
+
/**
|
|
111
|
+
* Insert manager-specific items (style dialog, timestamps, ...)
|
|
112
|
+
* between the resize item and the close item.
|
|
113
|
+
*/
|
|
114
|
+
buildExtraItems?: (ctx: DraggablePanelExtrasContext) => void;
|
|
115
|
+
/** Optional: vertical gap (px) between button and menu. */
|
|
116
|
+
menuGap?: number;
|
|
117
|
+
/** Optional: breathing room (px) before the menu flips above. */
|
|
118
|
+
menuSpaceReserve?: number;
|
|
119
|
+
/**
|
|
120
|
+
* Where to attach the mode-feedback badge while keyboard drag /
|
|
121
|
+
* pointer resize mode is active. If omitted, {@link DraggablePanel.showBadge}
|
|
122
|
+
* is a no-op (the manager effectively opts out of the badge).
|
|
123
|
+
*/
|
|
124
|
+
getBadgeHost?: () => HTMLElement | null;
|
|
125
|
+
/**
|
|
126
|
+
* CSS class for the badge element. Defaults to
|
|
127
|
+
* `{classPrefix}-{namespace}-mode-badge`. Sign-language passes a
|
|
128
|
+
* shorter class (`vidply-sign-mode-badge`) to preserve existing
|
|
129
|
+
* CSS. Transcript uses the default.
|
|
130
|
+
*/
|
|
131
|
+
badgeClass?: string;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export class DraggablePanel {
|
|
135
|
+
private readonly opts: DraggablePanelOptions;
|
|
136
|
+
|
|
137
|
+
/** Populated lazily on first `show()`. */
|
|
138
|
+
settingsMenu: HTMLElement | null = null;
|
|
139
|
+
settingsMenuVisible = false;
|
|
140
|
+
dragOptionButton: HTMLElement | null = null;
|
|
141
|
+
dragOptionText: Element | null = null;
|
|
142
|
+
resizeOptionButton: HTMLElement | null = null;
|
|
143
|
+
resizeOptionText: Element | null = null;
|
|
144
|
+
|
|
145
|
+
private _justOpened = false;
|
|
146
|
+
private _justOpenedTimer: ReturnType<typeof setTimeout> | null = null;
|
|
147
|
+
private _keyHandler: ((e: KeyboardEvent) => void) | null = null;
|
|
148
|
+
private _documentClick: ((e: MouseEvent) => void) | null = null;
|
|
149
|
+
private _documentClickAdded = false;
|
|
150
|
+
private _modeBadge: HTMLElement | null = null;
|
|
151
|
+
|
|
152
|
+
constructor(opts: DraggablePanelOptions) {
|
|
153
|
+
this.opts = opts;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** True while the just-opened debounce window (prevents the same
|
|
157
|
+
* click that opened the menu from also closing it via document
|
|
158
|
+
* `mousedown` / `click`). */
|
|
159
|
+
get justOpened(): boolean {
|
|
160
|
+
return this._justOpened;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
get classPrefix(): string {
|
|
164
|
+
return this.opts.player.options.classPrefix;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
get menuClass(): string {
|
|
168
|
+
return `${this.classPrefix}-${this.opts.namespace}-settings-menu`;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
get itemClass(): string {
|
|
172
|
+
return `${this.classPrefix}-${this.opts.namespace}-settings-item`;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Show the menu. First call creates the DOM; subsequent calls reuse
|
|
177
|
+
* it. Refreshes menu item state from the current draggable.
|
|
178
|
+
*/
|
|
179
|
+
show(): void {
|
|
180
|
+
this._markJustOpened(350);
|
|
181
|
+
this._ensureDocumentClickHandler();
|
|
182
|
+
|
|
183
|
+
if (this.settingsMenu) {
|
|
184
|
+
this.settingsMenu.style.display = 'block';
|
|
185
|
+
this.settingsMenuVisible = true;
|
|
186
|
+
this.opts.settingsButton.setAttribute('aria-expanded', 'true');
|
|
187
|
+
this._attachKeyboardNavigation();
|
|
188
|
+
this._positionImmediate();
|
|
189
|
+
this.refreshState();
|
|
190
|
+
focusFirstMenuItem(this.settingsMenu, `.${this.itemClass}`);
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
this._createMenu();
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Hide the menu. By default returns focus to the settings button;
|
|
199
|
+
* callers can opt out when the next interaction should land
|
|
200
|
+
* elsewhere (e.g. on the wrapper after enabling drag mode).
|
|
201
|
+
*/
|
|
202
|
+
hide({ focusButton = true }: { focusButton?: boolean } = {}): void {
|
|
203
|
+
if (!this.settingsMenu) return;
|
|
204
|
+
|
|
205
|
+
this.settingsMenu.style.display = 'none';
|
|
206
|
+
this.settingsMenuVisible = false;
|
|
207
|
+
this._clearJustOpened();
|
|
208
|
+
|
|
209
|
+
if (this._keyHandler) {
|
|
210
|
+
this.settingsMenu.removeEventListener('keydown', this._keyHandler, true);
|
|
211
|
+
this._keyHandler = null;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const items = this.settingsMenu.querySelectorAll<HTMLElement>(`.${this.itemClass}`);
|
|
215
|
+
items.forEach((item) => item.setAttribute('tabindex', '-1'));
|
|
216
|
+
|
|
217
|
+
const { settingsButton } = this.opts;
|
|
218
|
+
settingsButton.setAttribute('aria-expanded', 'false');
|
|
219
|
+
if (focusButton) {
|
|
220
|
+
settingsButton.focus({ preventScroll: true });
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
toggle(): void {
|
|
225
|
+
if (this.settingsMenuVisible) {
|
|
226
|
+
this.hide();
|
|
227
|
+
} else {
|
|
228
|
+
this.show();
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/** Set a short "just opened" guard so the document-click handler
|
|
233
|
+
* attached for outside-dismissal ignores the originating click. */
|
|
234
|
+
markJustOpenedForClick(): void {
|
|
235
|
+
if (this._documentClick) {
|
|
236
|
+
this._markJustOpened(100);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/** Refresh the drag and resize toggle item state from the draggable. */
|
|
241
|
+
refreshState(): void {
|
|
242
|
+
this.refreshDragState();
|
|
243
|
+
this.refreshResizeState();
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
refreshDragState(): void {
|
|
247
|
+
const draggable = this.opts.getDraggable();
|
|
248
|
+
updateToggleMenuItem(this.dragOptionButton, this.dragOptionText, {
|
|
249
|
+
enabled: Boolean(draggable?.keyboardDragMode),
|
|
250
|
+
enabledText: i18n.t(this.opts.i18nKeys.disableDrag),
|
|
251
|
+
disabledText: i18n.t(this.opts.i18nKeys.enableDrag),
|
|
252
|
+
enabledAria: i18n.t(this.opts.i18nKeys.disableDragAria),
|
|
253
|
+
disabledAria: i18n.t(this.opts.i18nKeys.enableDragAria),
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
refreshResizeState(): void {
|
|
258
|
+
const draggable = this.opts.getDraggable();
|
|
259
|
+
updateToggleMenuItem(this.resizeOptionButton, this.resizeOptionText, {
|
|
260
|
+
enabled: Boolean(draggable?.pointerResizeMode),
|
|
261
|
+
enabledText: i18n.t(this.opts.i18nKeys.disableResize),
|
|
262
|
+
disabledText: i18n.t(this.opts.i18nKeys.enableResize),
|
|
263
|
+
enabledAria: i18n.t(this.opts.i18nKeys.disableResizeAria),
|
|
264
|
+
disabledAria: i18n.t(this.opts.i18nKeys.enableResizeAria),
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Show a persistent mode-feedback badge (e.g. "Drag mode: arrow
|
|
270
|
+
* keys to move, Esc to exit") anchored to the host element
|
|
271
|
+
* returned by `getBadgeHost`. Replaces any previous badge. The
|
|
272
|
+
* badge is a real DOM element (not a CSS pseudo-element) so its
|
|
273
|
+
* text is translatable, selectable, visible under high-contrast
|
|
274
|
+
* themes, and reflected in browser translation overlays.
|
|
275
|
+
*
|
|
276
|
+
* Marked `aria-hidden` because the accompanying live-region
|
|
277
|
+
* announcement (the manager's responsibility) already conveys the
|
|
278
|
+
* state change to assistive tech.
|
|
279
|
+
*/
|
|
280
|
+
showBadge(text: string): void {
|
|
281
|
+
const host = this.opts.getBadgeHost?.();
|
|
282
|
+
if (!host) return;
|
|
283
|
+
this.hideBadge();
|
|
284
|
+
|
|
285
|
+
const className =
|
|
286
|
+
this.opts.badgeClass ??
|
|
287
|
+
`${this.classPrefix}-${this.opts.namespace}-mode-badge`;
|
|
288
|
+
const badge = DOMUtils.createElement('span', {
|
|
289
|
+
className,
|
|
290
|
+
textContent: text,
|
|
291
|
+
attributes: { 'aria-hidden': 'true' },
|
|
292
|
+
});
|
|
293
|
+
host.appendChild(badge);
|
|
294
|
+
this._modeBadge = badge;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/** Remove the mode-feedback badge if one is showing. */
|
|
298
|
+
hideBadge(): void {
|
|
299
|
+
if (this._modeBadge && this._modeBadge.parentNode) {
|
|
300
|
+
this._modeBadge.remove();
|
|
301
|
+
}
|
|
302
|
+
this._modeBadge = null;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/** RAF-deferred reposition (e.g. after a panel resize). */
|
|
306
|
+
reposition(): void {
|
|
307
|
+
positionSettingsMenuDeferred(this.settingsMenu, this.opts.settingsButton, {
|
|
308
|
+
align: this.opts.menuAlign,
|
|
309
|
+
gap: this.opts.menuGap ?? 4,
|
|
310
|
+
spaceReserve: this.opts.menuSpaceReserve ?? 20,
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Tear down any DOM/listeners owned by this panel. Safe to call
|
|
316
|
+
* multiple times. Callers must still drop their own references.
|
|
317
|
+
*/
|
|
318
|
+
destroy(): void {
|
|
319
|
+
if (this._justOpenedTimer) {
|
|
320
|
+
clearTimeout(this._justOpenedTimer);
|
|
321
|
+
this._justOpenedTimer = null;
|
|
322
|
+
}
|
|
323
|
+
this._justOpened = false;
|
|
324
|
+
|
|
325
|
+
this.hideBadge();
|
|
326
|
+
|
|
327
|
+
if (this.settingsMenu) {
|
|
328
|
+
if (this._keyHandler) {
|
|
329
|
+
this.settingsMenu.removeEventListener('keydown', this._keyHandler, true);
|
|
330
|
+
}
|
|
331
|
+
this.settingsMenu.remove();
|
|
332
|
+
this.settingsMenu = null;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
this._keyHandler = null;
|
|
336
|
+
this.settingsMenuVisible = false;
|
|
337
|
+
this.dragOptionButton = null;
|
|
338
|
+
this.dragOptionText = null;
|
|
339
|
+
this.resizeOptionButton = null;
|
|
340
|
+
this.resizeOptionText = null;
|
|
341
|
+
|
|
342
|
+
// The document-click listener was attached with the player's
|
|
343
|
+
// lifecycleSignal, so it is cleaned up automatically. We clear
|
|
344
|
+
// the local reference so a later show() re-installs.
|
|
345
|
+
this._documentClick = null;
|
|
346
|
+
this._documentClickAdded = false;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
private _createMenu(): void {
|
|
350
|
+
const { player, settingsButton, i18nKeys } = this.opts;
|
|
351
|
+
|
|
352
|
+
const menu = DOMUtils.createElement('div', {
|
|
353
|
+
className: this.menuClass,
|
|
354
|
+
attributes: { role: 'menu' },
|
|
355
|
+
});
|
|
356
|
+
this.settingsMenu = menu;
|
|
357
|
+
|
|
358
|
+
const dragOption = createMenuItem({
|
|
359
|
+
classPrefix: this.classPrefix,
|
|
360
|
+
itemClass: this.itemClass,
|
|
361
|
+
icon: 'move',
|
|
362
|
+
label: i18nKeys.enableDrag,
|
|
363
|
+
hasTextClass: true,
|
|
364
|
+
onClick: (event) => {
|
|
365
|
+
event.preventDefault();
|
|
366
|
+
event.stopPropagation();
|
|
367
|
+
this.opts.onDragItemClick(this);
|
|
368
|
+
this.refreshState();
|
|
369
|
+
},
|
|
370
|
+
});
|
|
371
|
+
dragOption.setAttribute('role', 'switch');
|
|
372
|
+
dragOption.setAttribute('aria-checked', 'false');
|
|
373
|
+
dragOption.setAttribute('data-setting', 'keyboard-drag');
|
|
374
|
+
this._stripInlineTooltip(dragOption);
|
|
375
|
+
this.dragOptionButton = dragOption;
|
|
376
|
+
this.dragOptionText = dragOption.querySelector(`.${this.classPrefix}-settings-text`);
|
|
377
|
+
|
|
378
|
+
const resizeOption = createMenuItem({
|
|
379
|
+
classPrefix: this.classPrefix,
|
|
380
|
+
itemClass: this.itemClass,
|
|
381
|
+
icon: 'resize',
|
|
382
|
+
label: i18nKeys.enableResize,
|
|
383
|
+
hasTextClass: true,
|
|
384
|
+
onClick: (event) => {
|
|
385
|
+
event.preventDefault();
|
|
386
|
+
event.stopPropagation();
|
|
387
|
+
this.opts.onResizeItemClick(this);
|
|
388
|
+
this.refreshState();
|
|
389
|
+
},
|
|
390
|
+
});
|
|
391
|
+
resizeOption.setAttribute('role', 'switch');
|
|
392
|
+
resizeOption.setAttribute('aria-checked', 'false');
|
|
393
|
+
this._stripInlineTooltip(resizeOption);
|
|
394
|
+
this.resizeOptionButton = resizeOption;
|
|
395
|
+
this.resizeOptionText = resizeOption.querySelector(`.${this.classPrefix}-settings-text`);
|
|
396
|
+
|
|
397
|
+
menu.appendChild(dragOption);
|
|
398
|
+
menu.appendChild(resizeOption);
|
|
399
|
+
|
|
400
|
+
if (this.opts.buildExtraItems) {
|
|
401
|
+
this.opts.buildExtraItems({
|
|
402
|
+
menu,
|
|
403
|
+
itemClass: this.itemClass,
|
|
404
|
+
classPrefix: this.classPrefix,
|
|
405
|
+
stripInlineTooltip: (item) => this._stripInlineTooltip(item),
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
const closeOption = createMenuItem({
|
|
410
|
+
classPrefix: this.classPrefix,
|
|
411
|
+
itemClass: this.itemClass,
|
|
412
|
+
icon: 'close',
|
|
413
|
+
label: i18nKeys.closeMenu,
|
|
414
|
+
onClick: () => this.hide(),
|
|
415
|
+
});
|
|
416
|
+
this._stripInlineTooltip(closeOption);
|
|
417
|
+
menu.appendChild(closeOption);
|
|
418
|
+
|
|
419
|
+
menu.style.visibility = 'hidden';
|
|
420
|
+
menu.style.display = 'block';
|
|
421
|
+
|
|
422
|
+
const parent = this.opts.getMenuParent();
|
|
423
|
+
if (settingsButton.parentNode) {
|
|
424
|
+
settingsButton.insertAdjacentElement('afterend', menu);
|
|
425
|
+
} else if (parent) {
|
|
426
|
+
parent.appendChild(menu);
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
this._positionImmediate();
|
|
430
|
+
|
|
431
|
+
requestAnimationFrame(() => {
|
|
432
|
+
if (this.settingsMenu) {
|
|
433
|
+
this.settingsMenu.style.visibility = 'visible';
|
|
434
|
+
}
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
this._attachKeyboardNavigation();
|
|
438
|
+
|
|
439
|
+
this.settingsMenuVisible = true;
|
|
440
|
+
settingsButton.setAttribute('aria-expanded', 'true');
|
|
441
|
+
this.refreshState();
|
|
442
|
+
|
|
443
|
+
focusFirstMenuItem(menu, `.${this.itemClass}`);
|
|
444
|
+
|
|
445
|
+
// Don't reveal the fact that we created the menu to callers that
|
|
446
|
+
// didn't trigger show() explicitly — but this is always driven
|
|
447
|
+
// by a show() so the `player` reference is just for
|
|
448
|
+
// documentation clarity here.
|
|
449
|
+
void player;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
private _attachKeyboardNavigation(): void {
|
|
453
|
+
const menu = this.settingsMenu;
|
|
454
|
+
if (!menu) return;
|
|
455
|
+
if (this._keyHandler) {
|
|
456
|
+
menu.removeEventListener('keydown', this._keyHandler, true);
|
|
457
|
+
}
|
|
458
|
+
const handler = attachMenuKeyboardNavigation(
|
|
459
|
+
menu,
|
|
460
|
+
this.opts.settingsButton,
|
|
461
|
+
`.${this.itemClass}`,
|
|
462
|
+
() => this.hide({ focusButton: true })
|
|
463
|
+
);
|
|
464
|
+
this._keyHandler = handler ?? null;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
private _positionImmediate(): void {
|
|
468
|
+
positionSettingsMenu(this.settingsMenu, this.opts.settingsButton, {
|
|
469
|
+
align: this.opts.menuAlign,
|
|
470
|
+
gap: this.opts.menuGap ?? 4,
|
|
471
|
+
spaceReserve: this.opts.menuSpaceReserve ?? 20,
|
|
472
|
+
});
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/**
|
|
476
|
+
* Remove tooltip and duplicate button-text nodes from a menu item.
|
|
477
|
+
* `createMenuItem` is used both for toolbar buttons (which want a
|
|
478
|
+
* tooltip) and for settings-menu rows (which show the same text
|
|
479
|
+
* inline). This strips the duplicated pieces so screen readers
|
|
480
|
+
* don't read the label twice.
|
|
481
|
+
*/
|
|
482
|
+
private _stripInlineTooltip(item: HTMLElement): void {
|
|
483
|
+
const tooltip = item.querySelector(`.${this.classPrefix}-tooltip`);
|
|
484
|
+
if (tooltip) tooltip.remove();
|
|
485
|
+
const buttonText = item.querySelector(`.${this.classPrefix}-button-text`);
|
|
486
|
+
if (buttonText) buttonText.remove();
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
private _markJustOpened(durationMs: number): void {
|
|
490
|
+
this._justOpened = true;
|
|
491
|
+
if (this._justOpenedTimer) {
|
|
492
|
+
clearTimeout(this._justOpenedTimer);
|
|
493
|
+
}
|
|
494
|
+
this._justOpenedTimer = setTimeout(() => {
|
|
495
|
+
this._justOpened = false;
|
|
496
|
+
this._justOpenedTimer = null;
|
|
497
|
+
}, durationMs);
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
private _clearJustOpened(): void {
|
|
501
|
+
this._justOpened = false;
|
|
502
|
+
if (this._justOpenedTimer) {
|
|
503
|
+
clearTimeout(this._justOpenedTimer);
|
|
504
|
+
this._justOpenedTimer = null;
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
private _ensureDocumentClickHandler(): void {
|
|
509
|
+
if (this._documentClickAdded) return;
|
|
510
|
+
|
|
511
|
+
this._documentClick = (event: MouseEvent) => {
|
|
512
|
+
if (this._justOpened) return;
|
|
513
|
+
const target = event.target as Node | null;
|
|
514
|
+
const { settingsButton } = this.opts;
|
|
515
|
+
|
|
516
|
+
if (settingsButton === target || (target && settingsButton.contains(target))) {
|
|
517
|
+
return;
|
|
518
|
+
}
|
|
519
|
+
if (this.settingsMenu && target && this.settingsMenu.contains(target)) {
|
|
520
|
+
return;
|
|
521
|
+
}
|
|
522
|
+
if (this.settingsMenuVisible) {
|
|
523
|
+
this.hide();
|
|
524
|
+
}
|
|
525
|
+
};
|
|
526
|
+
|
|
527
|
+
// Delay attach so the click that opened the menu doesn't instantly
|
|
528
|
+
// close it even if the justOpened flag somehow races.
|
|
529
|
+
setTimeout(() => {
|
|
530
|
+
const handler = this._documentClick;
|
|
531
|
+
if (!handler) return;
|
|
532
|
+
document.addEventListener('mousedown', handler, {
|
|
533
|
+
capture: true,
|
|
534
|
+
signal: this.opts.player.lifecycleSignal,
|
|
535
|
+
});
|
|
536
|
+
this._documentClickAdded = true;
|
|
537
|
+
}, 300);
|
|
538
|
+
}
|
|
539
|
+
}
|