ts-video-player 0.1.0
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/LICENSE.md +21 -0
- package/README.md +438 -0
- package/dist/cdn.d.ts +10 -0
- package/dist/cdn.d.ts.map +1 -0
- package/dist/cdn.min.js +1046 -0
- package/dist/cdn.min.js.map +54 -0
- package/dist/cdn.test.d.ts +2 -0
- package/dist/cdn.test.d.ts.map +1 -0
- package/dist/chunk-36x4yhjn.js +765 -0
- package/dist/chunk-36x4yhjn.js.map +12 -0
- package/dist/chunk-eajts2jv.js +4243 -0
- package/dist/chunk-eajts2jv.js.map +42 -0
- package/dist/chunk-z1ehbz9f.js +380 -0
- package/dist/chunk-z1ehbz9f.js.map +19 -0
- package/dist/config.d.ts +9 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/core/audio-gain.d.ts +67 -0
- package/dist/core/audio-gain.d.ts.map +1 -0
- package/dist/core/caption-styles.d.ts +121 -0
- package/dist/core/caption-styles.d.ts.map +1 -0
- package/dist/core/chapters.d.ts +150 -0
- package/dist/core/chapters.d.ts.map +1 -0
- package/dist/core/error-overlay.d.ts +71 -0
- package/dist/core/error-overlay.d.ts.map +1 -0
- package/dist/core/events.d.ts +119 -0
- package/dist/core/events.d.ts.map +1 -0
- package/dist/core/features.d.ts +64 -0
- package/dist/core/features.d.ts.map +1 -0
- package/dist/core/i18n.d.ts +132 -0
- package/dist/core/i18n.d.ts.map +1 -0
- package/dist/core/index.d.ts +37 -0
- package/dist/core/index.d.ts.map +1 -0
- package/dist/core/live.d.ts +130 -0
- package/dist/core/live.d.ts.map +1 -0
- package/dist/core/media-session.d.ts +38 -0
- package/dist/core/media-session.d.ts.map +1 -0
- package/dist/core/orientation.d.ts +22 -0
- package/dist/core/orientation.d.ts.map +1 -0
- package/dist/core/popover.d.ts +42 -0
- package/dist/core/popover.d.ts.map +1 -0
- package/dist/core/remote-playback.d.ts +195 -0
- package/dist/core/remote-playback.d.ts.map +1 -0
- package/dist/core/settings-menu.d.ts +132 -0
- package/dist/core/settings-menu.d.ts.map +1 -0
- package/dist/core/spinner.d.ts +67 -0
- package/dist/core/spinner.d.ts.map +1 -0
- package/dist/core/state.d.ts +142 -0
- package/dist/core/state.d.ts.map +1 -0
- package/dist/core/text-tracks.d.ts +162 -0
- package/dist/core/text-tracks.d.ts.map +1 -0
- package/dist/core/themes.d.ts +213 -0
- package/dist/core/themes.d.ts.map +1 -0
- package/dist/core/thumbnails.d.ts +124 -0
- package/dist/core/thumbnails.d.ts.map +1 -0
- package/dist/core/tooltips.d.ts +100 -0
- package/dist/core/tooltips.d.ts.map +1 -0
- package/dist/elements/base.d.ts +3 -0
- package/dist/elements/base.d.ts.map +1 -0
- package/dist/elements/index.d.ts +33 -0
- package/dist/elements/index.d.ts.map +1 -0
- package/dist/elements/index.js +55 -0
- package/dist/elements/index.js.map +9 -0
- package/dist/elements/media-airplay-button.d.ts +13 -0
- package/dist/elements/media-airplay-button.d.ts.map +1 -0
- package/dist/elements/media-error-display.d.ts +13 -0
- package/dist/elements/media-error-display.d.ts.map +1 -0
- package/dist/elements/media-fullscreen-button.d.ts +13 -0
- package/dist/elements/media-fullscreen-button.d.ts.map +1 -0
- package/dist/elements/media-live-button.d.ts +13 -0
- package/dist/elements/media-live-button.d.ts.map +1 -0
- package/dist/elements/media-mute-button.d.ts +13 -0
- package/dist/elements/media-mute-button.d.ts.map +1 -0
- package/dist/elements/media-pip-button.d.ts +13 -0
- package/dist/elements/media-pip-button.d.ts.map +1 -0
- package/dist/elements/media-play-button.d.ts +14 -0
- package/dist/elements/media-play-button.d.ts.map +1 -0
- package/dist/elements/media-playback-rate-button.d.ts +10 -0
- package/dist/elements/media-playback-rate-button.d.ts.map +1 -0
- package/dist/elements/media-preview-time.d.ts +18 -0
- package/dist/elements/media-preview-time.d.ts.map +1 -0
- package/dist/elements/media-progress-bar.d.ts +16 -0
- package/dist/elements/media-progress-bar.d.ts.map +1 -0
- package/dist/elements/media-remote-playback-button.d.ts +16 -0
- package/dist/elements/media-remote-playback-button.d.ts.map +1 -0
- package/dist/elements/media-seek-button.d.ts +9 -0
- package/dist/elements/media-seek-button.d.ts.map +1 -0
- package/dist/elements/media-settings-menu.d.ts +32 -0
- package/dist/elements/media-settings-menu.d.ts.map +1 -0
- package/dist/elements/media-time-display.d.ts +21 -0
- package/dist/elements/media-time-display.d.ts.map +1 -0
- package/dist/elements/media-time-group.d.ts +22 -0
- package/dist/elements/media-time-group.d.ts.map +1 -0
- package/dist/elements/media-time-separator.d.ts +13 -0
- package/dist/elements/media-time-separator.d.ts.map +1 -0
- package/dist/elements/media-volume-slider.d.ts +16 -0
- package/dist/elements/media-volume-slider.d.ts.map +1 -0
- package/dist/elements/utils.d.ts +27 -0
- package/dist/elements/utils.d.ts.map +1 -0
- package/dist/elements/video-player.d.ts +23 -0
- package/dist/elements/video-player.d.ts.map +1 -0
- package/dist/elements/video-skin.d.ts +23 -0
- package/dist/elements/video-skin.d.ts.map +1 -0
- package/dist/features/captions.d.ts +8 -0
- package/dist/features/captions.d.ts.map +1 -0
- package/dist/features/create-player.d.ts +45 -0
- package/dist/features/create-player.d.ts.map +1 -0
- package/dist/features/fullscreen.d.ts +8 -0
- package/dist/features/fullscreen.d.ts.map +1 -0
- package/dist/features/gestures.d.ts +8 -0
- package/dist/features/gestures.d.ts.map +1 -0
- package/dist/features/index.d.ts +26 -0
- package/dist/features/index.d.ts.map +1 -0
- package/dist/features/index.js +31 -0
- package/dist/features/index.js.map +9 -0
- package/dist/features/keyboard.d.ts +8 -0
- package/dist/features/keyboard.d.ts.map +1 -0
- package/dist/features/pip.d.ts +8 -0
- package/dist/features/pip.d.ts.map +1 -0
- package/dist/features/playback.d.ts +8 -0
- package/dist/features/playback.d.ts.map +1 -0
- package/dist/features/quality.d.ts +8 -0
- package/dist/features/quality.d.ts.map +1 -0
- package/dist/features/types.d.ts +26 -0
- package/dist/features/types.d.ts.map +1 -0
- package/dist/features/volume.d.ts +8 -0
- package/dist/features/volume.d.ts.map +1 -0
- package/dist/index.d.ts +84 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +8149 -0
- package/dist/index.js.map +31 -0
- package/dist/layouts/index.d.ts +58 -0
- package/dist/layouts/index.d.ts.map +1 -0
- package/dist/player.d.ts +90 -0
- package/dist/player.d.ts.map +1 -0
- package/dist/plugins/ads.d.ts +311 -0
- package/dist/plugins/ads.d.ts.map +1 -0
- package/dist/plugins/analytics.d.ts +203 -0
- package/dist/plugins/analytics.d.ts.map +1 -0
- package/dist/plugins/end-screen.d.ts +184 -0
- package/dist/plugins/end-screen.d.ts.map +1 -0
- package/dist/plugins/index.d.ts +13 -0
- package/dist/plugins/index.d.ts.map +1 -0
- package/dist/plugins/index.js +20 -0
- package/dist/plugins/index.js.map +9 -0
- package/dist/plugins/skip-segments.d.ts +159 -0
- package/dist/plugins/skip-segments.d.ts.map +1 -0
- package/dist/plugins/watermark.d.ts +157 -0
- package/dist/plugins/watermark.d.ts.map +1 -0
- package/dist/providers/base.d.ts +56 -0
- package/dist/providers/base.d.ts.map +1 -0
- package/dist/providers/dash.d.ts +94 -0
- package/dist/providers/dash.d.ts.map +1 -0
- package/dist/providers/hls.d.ts +182 -0
- package/dist/providers/hls.d.ts.map +1 -0
- package/dist/providers/html5.d.ts +64 -0
- package/dist/providers/html5.d.ts.map +1 -0
- package/dist/providers/index.d.ts +41 -0
- package/dist/providers/index.d.ts.map +1 -0
- package/dist/providers/vimeo.d.ts +157 -0
- package/dist/providers/vimeo.d.ts.map +1 -0
- package/dist/providers/youtube.d.ts +134 -0
- package/dist/providers/youtube.d.ts.map +1 -0
- package/dist/stx/index.d.ts +59 -0
- package/dist/stx/index.d.ts.map +1 -0
- package/dist/stx/index.js +480 -0
- package/dist/stx/index.js.map +11 -0
- package/dist/types.d.ts +724 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/ui/components.d.ts +121 -0
- package/dist/ui/components.d.ts.map +1 -0
- package/dist/ui/index.d.ts +9 -0
- package/dist/ui/index.d.ts.map +1 -0
- package/dist/ui/index.js +16 -0
- package/dist/ui/index.js.map +9 -0
- package/package.json +109 -0
- package/src/cdn.test.ts +10 -0
- package/src/cdn.ts +60 -0
- package/src/config.ts +25 -0
- package/src/core/audio-gain.ts +171 -0
- package/src/core/caption-styles.ts +568 -0
- package/src/core/chapters.ts +551 -0
- package/src/core/error-overlay.ts +354 -0
- package/src/core/events.ts +552 -0
- package/src/core/features.ts +287 -0
- package/src/core/i18n.ts +590 -0
- package/src/core/index.ts +249 -0
- package/src/core/live.ts +472 -0
- package/src/core/media-session.ts +137 -0
- package/src/core/orientation.ts +57 -0
- package/src/core/popover.ts +143 -0
- package/src/core/remote-playback.ts +661 -0
- package/src/core/settings-menu.ts +630 -0
- package/src/core/spinner.ts +310 -0
- package/src/core/state.ts +476 -0
- package/src/core/text-tracks.ts +599 -0
- package/src/core/themes.ts +607 -0
- package/src/core/thumbnails.ts +457 -0
- package/src/core/tooltips.ts +370 -0
- package/src/elements/base.ts +2 -0
- package/src/elements/index.ts +82 -0
- package/src/elements/media-airplay-button.ts +78 -0
- package/src/elements/media-error-display.ts +47 -0
- package/src/elements/media-fullscreen-button.ts +72 -0
- package/src/elements/media-live-button.ts +65 -0
- package/src/elements/media-mute-button.ts +69 -0
- package/src/elements/media-pip-button.ts +66 -0
- package/src/elements/media-play-button.ts +69 -0
- package/src/elements/media-playback-rate-button.ts +59 -0
- package/src/elements/media-preview-time.ts +75 -0
- package/src/elements/media-progress-bar.ts +200 -0
- package/src/elements/media-remote-playback-button.ts +108 -0
- package/src/elements/media-seek-button.ts +50 -0
- package/src/elements/media-settings-menu.ts +302 -0
- package/src/elements/media-time-display.ts +99 -0
- package/src/elements/media-time-group.ts +41 -0
- package/src/elements/media-time-separator.ts +35 -0
- package/src/elements/media-volume-slider.ts +140 -0
- package/src/elements/utils.ts +82 -0
- package/src/elements/video-player.ts +175 -0
- package/src/elements/video-skin.ts +127 -0
- package/src/features/captions.ts +56 -0
- package/src/features/create-player.ts +91 -0
- package/src/features/fullscreen.ts +44 -0
- package/src/features/gestures.ts +44 -0
- package/src/features/index.ts +44 -0
- package/src/features/keyboard.ts +89 -0
- package/src/features/pip.ts +47 -0
- package/src/features/playback.ts +68 -0
- package/src/features/quality.ts +19 -0
- package/src/features/types.ts +28 -0
- package/src/features/volume.ts +46 -0
- package/src/index.ts +635 -0
- package/src/layouts/index.ts +672 -0
- package/src/player.ts +821 -0
- package/src/plugins/ads.ts +1045 -0
- package/src/plugins/analytics.ts +673 -0
- package/src/plugins/end-screen.ts +793 -0
- package/src/plugins/index.ts +61 -0
- package/src/plugins/skip-segments.ts +537 -0
- package/src/plugins/watermark.ts +537 -0
- package/src/providers/base.ts +197 -0
- package/src/providers/dash.ts +588 -0
- package/src/providers/hls.ts +624 -0
- package/src/providers/html5.ts +473 -0
- package/src/providers/index.ts +97 -0
- package/src/providers/vimeo.ts +533 -0
- package/src/providers/youtube.ts +542 -0
- package/src/stx/index.ts +606 -0
- package/src/types.ts +874 -0
- package/src/ui/components.ts +910 -0
- package/src/ui/index.ts +29 -0
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ts-video-player Media Session API Integration
|
|
3
|
+
*
|
|
4
|
+
* Provides lock screen / notification controls via navigator.mediaSession.
|
|
5
|
+
*
|
|
6
|
+
* @module core/media-session
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { StateStore } from './state'
|
|
10
|
+
|
|
11
|
+
export interface MediaSessionOptions {
|
|
12
|
+
/** Title to display */
|
|
13
|
+
title?: string
|
|
14
|
+
/** Artist name */
|
|
15
|
+
artist?: string
|
|
16
|
+
/** Album name */
|
|
17
|
+
album?: string
|
|
18
|
+
/** Artwork images for lock screen */
|
|
19
|
+
artwork?: MediaImage[]
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Check if Media Session API is available
|
|
24
|
+
*/
|
|
25
|
+
export function isMediaSessionSupported(): boolean {
|
|
26
|
+
return typeof navigator !== 'undefined' && 'mediaSession' in navigator
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Create a Media Session controller that syncs player state to the browser's
|
|
31
|
+
* media session (lock screen controls, notification area, etc.)
|
|
32
|
+
*/
|
|
33
|
+
export function createMediaSession(
|
|
34
|
+
store: StateStore,
|
|
35
|
+
actions: {
|
|
36
|
+
play: () => void
|
|
37
|
+
pause: () => void
|
|
38
|
+
seekTo: (time: number) => void
|
|
39
|
+
seekBy: (offset: number) => void
|
|
40
|
+
stop: () => void
|
|
41
|
+
},
|
|
42
|
+
options?: MediaSessionOptions,
|
|
43
|
+
): () => void {
|
|
44
|
+
if (!isMediaSessionSupported()) return () => {}
|
|
45
|
+
|
|
46
|
+
const session = navigator.mediaSession
|
|
47
|
+
|
|
48
|
+
// Set metadata
|
|
49
|
+
if (options) {
|
|
50
|
+
updateMetadata(options)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Action handlers
|
|
54
|
+
const handlers: Array<[MediaSessionAction, MediaSessionActionHandler]> = [
|
|
55
|
+
['play', () => actions.play()],
|
|
56
|
+
['pause', () => actions.pause()],
|
|
57
|
+
['stop', () => actions.stop()],
|
|
58
|
+
['seekbackward', (details) => {
|
|
59
|
+
const offset = (details as MediaSessionActionDetails & { seekOffset?: number }).seekOffset || -10
|
|
60
|
+
actions.seekBy(offset)
|
|
61
|
+
}],
|
|
62
|
+
['seekforward', (details) => {
|
|
63
|
+
const offset = (details as MediaSessionActionDetails & { seekOffset?: number }).seekOffset || 10
|
|
64
|
+
actions.seekBy(offset)
|
|
65
|
+
}],
|
|
66
|
+
['seekto', (details) => {
|
|
67
|
+
const seekTime = (details as MediaSessionActionDetails & { seekTime?: number }).seekTime
|
|
68
|
+
if (seekTime !== undefined) actions.seekTo(seekTime)
|
|
69
|
+
}],
|
|
70
|
+
]
|
|
71
|
+
|
|
72
|
+
for (const [action, handler] of handlers) {
|
|
73
|
+
try {
|
|
74
|
+
session.setActionHandler(action, handler)
|
|
75
|
+
} catch {
|
|
76
|
+
// Some actions may not be supported on all platforms
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Sync playback state
|
|
81
|
+
const unsubPaused = store.subscribe('paused', (state) => {
|
|
82
|
+
session.playbackState = state.paused ? 'paused' : 'playing'
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
// Sync position state
|
|
86
|
+
let positionTimer: ReturnType<typeof setInterval> | null = null
|
|
87
|
+
|
|
88
|
+
const updatePosition = () => {
|
|
89
|
+
const state = store.getState()
|
|
90
|
+
if (!isFinite(state.duration) || state.duration <= 0) return
|
|
91
|
+
try {
|
|
92
|
+
session.setPositionState({
|
|
93
|
+
duration: state.duration,
|
|
94
|
+
playbackRate: state.playbackRate,
|
|
95
|
+
position: Math.min(state.currentTime, state.duration),
|
|
96
|
+
})
|
|
97
|
+
} catch {
|
|
98
|
+
// Position state may fail if values are invalid
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const unsubPlaying = store.subscribe('playing', (state) => {
|
|
103
|
+
if (state.playing) {
|
|
104
|
+
updatePosition()
|
|
105
|
+
if (!positionTimer) {
|
|
106
|
+
positionTimer = setInterval(updatePosition, 1000)
|
|
107
|
+
}
|
|
108
|
+
} else {
|
|
109
|
+
if (positionTimer) { clearInterval(positionTimer); positionTimer = null }
|
|
110
|
+
updatePosition()
|
|
111
|
+
}
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
return () => {
|
|
115
|
+
if (positionTimer) clearInterval(positionTimer)
|
|
116
|
+
unsubPaused()
|
|
117
|
+
unsubPlaying()
|
|
118
|
+
|
|
119
|
+
// Clear action handlers
|
|
120
|
+
for (const [action] of handlers) {
|
|
121
|
+
try { session.setActionHandler(action, null) } catch { /* ignore */ }
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Update media session metadata
|
|
128
|
+
*/
|
|
129
|
+
export function updateMetadata(options: MediaSessionOptions): void {
|
|
130
|
+
if (!isMediaSessionSupported()) return
|
|
131
|
+
navigator.mediaSession.metadata = new MediaMetadata({
|
|
132
|
+
title: options.title || '',
|
|
133
|
+
artist: options.artist || '',
|
|
134
|
+
album: options.album || '',
|
|
135
|
+
artwork: options.artwork || [],
|
|
136
|
+
})
|
|
137
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ts-video-player Orientation Lock
|
|
3
|
+
*
|
|
4
|
+
* Auto-lock screen orientation to landscape when entering fullscreen on mobile.
|
|
5
|
+
*
|
|
6
|
+
* @module core/orientation
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
type OrientationType = 'landscape' | 'portrait' | 'any'
|
|
10
|
+
|
|
11
|
+
interface ScreenOrientationAPI {
|
|
12
|
+
lock(orientation: string): Promise<void>
|
|
13
|
+
unlock(): void
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Check if Screen Orientation API is available
|
|
18
|
+
*/
|
|
19
|
+
export function isOrientationLockSupported(): boolean {
|
|
20
|
+
return (
|
|
21
|
+
typeof screen !== 'undefined' &&
|
|
22
|
+
'orientation' in screen &&
|
|
23
|
+
typeof (screen.orientation as unknown as ScreenOrientationAPI).lock === 'function'
|
|
24
|
+
)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Lock the screen orientation (best-effort, silently fails if unsupported)
|
|
29
|
+
*/
|
|
30
|
+
export function lockOrientation(type: OrientationType): void {
|
|
31
|
+
if (!isOrientationLockSupported()) return
|
|
32
|
+
|
|
33
|
+
const orientationMap: Record<OrientationType, string> = {
|
|
34
|
+
landscape: 'landscape',
|
|
35
|
+
portrait: 'portrait',
|
|
36
|
+
any: 'any',
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
;(screen.orientation as unknown as ScreenOrientationAPI)
|
|
40
|
+
.lock(orientationMap[type])
|
|
41
|
+
.catch(() => {
|
|
42
|
+
// Orientation lock may fail if not in fullscreen or unsupported
|
|
43
|
+
})
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Unlock the screen orientation
|
|
48
|
+
*/
|
|
49
|
+
export function unlockOrientation(): void {
|
|
50
|
+
if (!isOrientationLockSupported()) return
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
;(screen.orientation as unknown as ScreenOrientationAPI).unlock()
|
|
54
|
+
} catch {
|
|
55
|
+
// Unlock may fail if lock was never acquired
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ts-video-player Popover Positioning
|
|
3
|
+
*
|
|
4
|
+
* Collision-aware positioning for menus and tooltips.
|
|
5
|
+
*
|
|
6
|
+
* @module core/popover
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export interface Rect {
|
|
10
|
+
top: number
|
|
11
|
+
left: number
|
|
12
|
+
bottom: number
|
|
13
|
+
right: number
|
|
14
|
+
width: number
|
|
15
|
+
height: number
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export type Placement = 'top' | 'bottom' | 'top-start' | 'top-end' | 'bottom-start' | 'bottom-end'
|
|
19
|
+
|
|
20
|
+
export interface PositionOptions {
|
|
21
|
+
/** Preferred placement */
|
|
22
|
+
placement?: Placement
|
|
23
|
+
/** Gap between anchor and popover in px */
|
|
24
|
+
offset?: number
|
|
25
|
+
/** Boundary element (defaults to viewport) */
|
|
26
|
+
boundary?: HTMLElement | null
|
|
27
|
+
/** Minimum distance from boundary edges in px */
|
|
28
|
+
padding?: number
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface PositionResult {
|
|
32
|
+
top: number
|
|
33
|
+
left: number
|
|
34
|
+
placement: Placement
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Calculate the position of a popover relative to an anchor element,
|
|
39
|
+
* flipping when it would overflow the boundary.
|
|
40
|
+
*/
|
|
41
|
+
export function computePosition(
|
|
42
|
+
anchor: HTMLElement,
|
|
43
|
+
popover: HTMLElement,
|
|
44
|
+
options: PositionOptions = {},
|
|
45
|
+
): PositionResult {
|
|
46
|
+
const {
|
|
47
|
+
placement = 'top',
|
|
48
|
+
offset = 4,
|
|
49
|
+
boundary = null,
|
|
50
|
+
padding = 8,
|
|
51
|
+
} = options
|
|
52
|
+
|
|
53
|
+
const anchorRect = anchor.getBoundingClientRect()
|
|
54
|
+
const popoverRect = popover.getBoundingClientRect()
|
|
55
|
+
|
|
56
|
+
const boundaryRect = boundary
|
|
57
|
+
? boundary.getBoundingClientRect()
|
|
58
|
+
: { top: 0, left: 0, bottom: window.innerHeight, right: window.innerWidth, width: window.innerWidth, height: window.innerHeight }
|
|
59
|
+
|
|
60
|
+
const availableTop = anchorRect.top - boundaryRect.top - padding
|
|
61
|
+
const availableBottom = boundaryRect.bottom - anchorRect.bottom - padding
|
|
62
|
+
|
|
63
|
+
// Determine vertical placement (flip if needed)
|
|
64
|
+
let vertical: 'top' | 'bottom'
|
|
65
|
+
if (placement.startsWith('top')) {
|
|
66
|
+
vertical = popoverRect.height + offset <= availableTop ? 'top' : 'bottom'
|
|
67
|
+
} else {
|
|
68
|
+
vertical = popoverRect.height + offset <= availableBottom ? 'bottom' : 'top'
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Calculate top
|
|
72
|
+
let top: number
|
|
73
|
+
if (vertical === 'top') {
|
|
74
|
+
top = anchorRect.top - popoverRect.height - offset
|
|
75
|
+
} else {
|
|
76
|
+
top = anchorRect.bottom + offset
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Determine horizontal alignment
|
|
80
|
+
let align: '' | '-start' | '-end' = ''
|
|
81
|
+
if (placement.endsWith('-start')) align = '-start'
|
|
82
|
+
else if (placement.endsWith('-end')) align = '-end'
|
|
83
|
+
|
|
84
|
+
// Calculate left
|
|
85
|
+
let left: number
|
|
86
|
+
switch (align) {
|
|
87
|
+
case '-start':
|
|
88
|
+
left = anchorRect.left
|
|
89
|
+
break
|
|
90
|
+
case '-end':
|
|
91
|
+
left = anchorRect.right - popoverRect.width
|
|
92
|
+
break
|
|
93
|
+
default:
|
|
94
|
+
left = anchorRect.left + (anchorRect.width - popoverRect.width) / 2
|
|
95
|
+
break
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Clamp horizontal to boundary
|
|
99
|
+
const minLeft = boundaryRect.left + padding
|
|
100
|
+
const maxLeft = boundaryRect.right - popoverRect.width - padding
|
|
101
|
+
left = Math.max(minLeft, Math.min(maxLeft, left))
|
|
102
|
+
|
|
103
|
+
// Clamp vertical to boundary
|
|
104
|
+
const minTop = boundaryRect.top + padding
|
|
105
|
+
const maxTop = boundaryRect.bottom - popoverRect.height - padding
|
|
106
|
+
top = Math.max(minTop, Math.min(maxTop, top))
|
|
107
|
+
|
|
108
|
+
const finalPlacement: Placement = `${vertical}${align}` as Placement
|
|
109
|
+
|
|
110
|
+
return { top, left, placement: finalPlacement }
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Apply computed position to a popover element using absolute positioning
|
|
115
|
+
* relative to an offset parent.
|
|
116
|
+
*/
|
|
117
|
+
export function applyPosition(
|
|
118
|
+
anchor: HTMLElement,
|
|
119
|
+
popover: HTMLElement,
|
|
120
|
+
options: PositionOptions = {},
|
|
121
|
+
): Placement {
|
|
122
|
+
// Temporarily make visible for measurement
|
|
123
|
+
const prevVisibility = popover.style.visibility
|
|
124
|
+
popover.style.visibility = 'hidden'
|
|
125
|
+
popover.style.display = 'block'
|
|
126
|
+
|
|
127
|
+
const result = computePosition(anchor, popover, options)
|
|
128
|
+
|
|
129
|
+
// Convert to relative coordinates if popover has an offset parent
|
|
130
|
+
const offsetParent = popover.offsetParent as HTMLElement | null
|
|
131
|
+
if (offsetParent) {
|
|
132
|
+
const parentRect = offsetParent.getBoundingClientRect()
|
|
133
|
+
popover.style.top = `${result.top - parentRect.top}px`
|
|
134
|
+
popover.style.left = `${result.left - parentRect.left}px`
|
|
135
|
+
} else {
|
|
136
|
+
popover.style.top = `${result.top}px`
|
|
137
|
+
popover.style.left = `${result.left}px`
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
popover.style.visibility = prevVisibility
|
|
141
|
+
|
|
142
|
+
return result.placement
|
|
143
|
+
}
|