@smartimpact-it/modern-video-embed 2.0.6 → 2.0.8
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/dist/components/BaseVideoEmbed.d.ts +27 -5
- package/dist/components/BaseVideoEmbed.d.ts.map +1 -1
- package/dist/components/BaseVideoEmbed.js +70 -0
- package/dist/components/BaseVideoEmbed.js.map +1 -1
- package/dist/components/VideoEmbed.d.ts +6 -0
- package/dist/components/VideoEmbed.d.ts.map +1 -1
- package/dist/components/VideoEmbed.js +45 -40
- package/dist/components/VideoEmbed.js.map +1 -1
- package/dist/components/VimeoEmbed.d.ts +7 -5
- package/dist/components/VimeoEmbed.d.ts.map +1 -1
- package/dist/components/VimeoEmbed.js +67 -97
- package/dist/components/VimeoEmbed.js.map +1 -1
- package/dist/components/VimeoEmbed.min.js +1 -1
- package/dist/components/YouTubeEmbed.d.ts +7 -6
- package/dist/components/YouTubeEmbed.d.ts.map +1 -1
- package/dist/components/YouTubeEmbed.js +65 -108
- package/dist/components/YouTubeEmbed.js.map +1 -1
- package/dist/components/YouTubeEmbed.min.js +1 -1
- package/dist/css/components.css +68 -42
- package/dist/css/components.css.map +1 -1
- package/dist/css/components.min.css +1 -1
- package/dist/css/main.css +68 -42
- package/dist/css/main.css.map +1 -1
- package/dist/css/main.min.css +1 -1
- package/dist/utils/APILoader.d.ts +55 -0
- package/dist/utils/APILoader.d.ts.map +1 -0
- package/dist/utils/APILoader.js +148 -0
- package/dist/utils/APILoader.js.map +1 -0
- package/package.json +1 -1
- package/src/components/BaseVideoEmbed.ts +107 -6
- package/src/components/VideoEmbed.ts +51 -42
- package/src/components/VimeoEmbed.ts +71 -113
- package/src/components/YouTubeEmbed.ts +71 -130
- package/src/styles/_embed-base.scss +35 -15
- package/src/styles/video-embed.scss +18 -0
- package/src/utils/APILoader.ts +192 -0
|
@@ -6,13 +6,16 @@
|
|
|
6
6
|
@use "shared-functions" as *;
|
|
7
7
|
|
|
8
8
|
// CSS Variables (common across all components)
|
|
9
|
+
// All variables are prefixed with --si-embed- to prevent conflicts
|
|
9
10
|
@mixin embed-css-variables {
|
|
10
|
-
--video-aspect-ratio: 56.25%; // Default 16:9
|
|
11
|
-
--
|
|
12
|
-
--
|
|
13
|
-
--
|
|
14
|
-
--
|
|
15
|
-
--
|
|
11
|
+
--si-embed-video-aspect-ratio: 56.25%; // Default 16:9
|
|
12
|
+
--si-embed-aspect-width: 16; // For dynamic aspect ratio calculations
|
|
13
|
+
--si-embed-aspect-height: 9; // For dynamic aspect ratio calculations
|
|
14
|
+
--si-embed-poster-object-fit: cover;
|
|
15
|
+
--si-embed-video-object-fit: contain;
|
|
16
|
+
--si-embed-control-button-size: 70px;
|
|
17
|
+
--si-embed-control-button-color: #ffffff;
|
|
18
|
+
--si-embed-overlay-background-color: rgba(0, 0, 0, 0.5);
|
|
16
19
|
}
|
|
17
20
|
|
|
18
21
|
// Base container styles
|
|
@@ -20,7 +23,7 @@
|
|
|
20
23
|
display: block;
|
|
21
24
|
position: relative;
|
|
22
25
|
width: 100%;
|
|
23
|
-
padding-top: var(--video-aspect-ratio);
|
|
26
|
+
padding-top: var(--si-embed-video-aspect-ratio);
|
|
24
27
|
overflow: hidden;
|
|
25
28
|
background-color: #000;
|
|
26
29
|
}
|
|
@@ -33,7 +36,7 @@
|
|
|
33
36
|
width: 100%;
|
|
34
37
|
height: 100%;
|
|
35
38
|
border: none;
|
|
36
|
-
object-fit: var(--video-object-fit);
|
|
39
|
+
object-fit: var(--si-embed-video-object-fit);
|
|
37
40
|
}
|
|
38
41
|
|
|
39
42
|
// Poster image styles
|
|
@@ -46,7 +49,7 @@
|
|
|
46
49
|
left: 0;
|
|
47
50
|
width: 100%;
|
|
48
51
|
height: 100%;
|
|
49
|
-
object-fit: var(--poster-object-fit);
|
|
52
|
+
object-fit: var(--si-embed-poster-object-fit);
|
|
50
53
|
display: block;
|
|
51
54
|
cursor: pointer;
|
|
52
55
|
}
|
|
@@ -55,7 +58,7 @@
|
|
|
55
58
|
// Custom control button overlay
|
|
56
59
|
@mixin embed-button-overlay {
|
|
57
60
|
.button-overlay {
|
|
58
|
-
background: var(--overlay-background-color);
|
|
61
|
+
background: var(--si-embed-overlay-background-color);
|
|
59
62
|
cursor: pointer;
|
|
60
63
|
display: flex;
|
|
61
64
|
align-items: center;
|
|
@@ -86,8 +89,8 @@
|
|
|
86
89
|
display: flex;
|
|
87
90
|
align-items: center;
|
|
88
91
|
justify-content: center;
|
|
89
|
-
height: var(--control-button-size);
|
|
90
|
-
width: var(--control-button-size);
|
|
92
|
+
height: var(--si-embed-control-button-size);
|
|
93
|
+
width: var(--si-embed-control-button-size);
|
|
91
94
|
background-image: inline-svg(
|
|
92
95
|
'<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path fill="#ffffff" d="M8,5.14V19.14L19,12.14L8,5.14Z" /></svg>'
|
|
93
96
|
);
|
|
@@ -122,9 +125,13 @@
|
|
|
122
125
|
top: 50%;
|
|
123
126
|
left: 50%;
|
|
124
127
|
transform: translate(-50%, -50%);
|
|
125
|
-
aspect-ratio
|
|
128
|
+
// Use aspect-ratio with min-dimensions to ensure coverage
|
|
129
|
+
aspect-ratio: var(--si-embed-aspect-width) /
|
|
130
|
+
var(--si-embed-aspect-height);
|
|
131
|
+
// Start with auto sizing based on aspect ratio
|
|
126
132
|
width: auto;
|
|
127
133
|
height: auto;
|
|
134
|
+
// Ensure iframe covers entire container
|
|
128
135
|
min-width: 100%;
|
|
129
136
|
min-height: 100%;
|
|
130
137
|
pointer-events: none;
|
|
@@ -135,10 +142,23 @@
|
|
|
135
142
|
top: 50%;
|
|
136
143
|
left: 50%;
|
|
137
144
|
transform: translate(-50%, -50%);
|
|
145
|
+
// Use CSS custom properties for dynamic aspect ratio
|
|
138
146
|
width: 100%;
|
|
139
|
-
height:
|
|
147
|
+
height: calc(
|
|
148
|
+
(100% * var(--si-embed-aspect-height)) / var(--si-embed-aspect-width)
|
|
149
|
+
);
|
|
150
|
+
min-width: 100%;
|
|
151
|
+
min-height: 100%;
|
|
140
152
|
object-fit: cover;
|
|
141
153
|
pointer-events: none;
|
|
154
|
+
|
|
155
|
+
// Hide native media controls in background mode
|
|
156
|
+
&::-webkit-media-controls {
|
|
157
|
+
display: none !important;
|
|
158
|
+
}
|
|
159
|
+
&::-webkit-media-controls-enclosure {
|
|
160
|
+
display: none !important;
|
|
161
|
+
}
|
|
142
162
|
}
|
|
143
163
|
}
|
|
144
164
|
|
|
@@ -157,7 +177,7 @@
|
|
|
157
177
|
padding: 1rem;
|
|
158
178
|
|
|
159
179
|
.button {
|
|
160
|
-
--control-button-size: 40px;
|
|
180
|
+
--si-embed-control-button-size: 40px;
|
|
161
181
|
opacity: 0.7;
|
|
162
182
|
transition: opacity 0.2s ease;
|
|
163
183
|
&:hover {
|
|
@@ -11,6 +11,24 @@ video-embed {
|
|
|
11
11
|
// Video-specific styles only
|
|
12
12
|
video {
|
|
13
13
|
@include embed-media-base;
|
|
14
|
+
|
|
15
|
+
// Hide native controls when controls attribute is not set
|
|
16
|
+
&::-webkit-media-controls {
|
|
17
|
+
display: none !important;
|
|
18
|
+
}
|
|
19
|
+
&::-webkit-media-controls-enclosure {
|
|
20
|
+
display: none !important;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Show native controls only when explicitly enabled
|
|
25
|
+
&[controls] video {
|
|
26
|
+
&::-webkit-media-controls {
|
|
27
|
+
display: flex !important;
|
|
28
|
+
}
|
|
29
|
+
&::-webkit-media-controls-enclosure {
|
|
30
|
+
display: flex !important;
|
|
31
|
+
}
|
|
14
32
|
}
|
|
15
33
|
|
|
16
34
|
// Poster image specific to video (uses background-image pattern)
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared utility for loading external API scripts (YouTube, Vimeo, etc.)
|
|
3
|
+
* Provides retry mechanism, timeout handling, and deduplication.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export interface APILoaderConfig {
|
|
7
|
+
/** URL of the script to load */
|
|
8
|
+
scriptUrl: string;
|
|
9
|
+
/** Function to check if the API is ready (e.g., checking global variables) */
|
|
10
|
+
globalCheck: () => boolean;
|
|
11
|
+
/** Optional global callback function name (e.g., 'onYouTubeIframeAPIReady') */
|
|
12
|
+
globalCallback?: string;
|
|
13
|
+
/** Timeout in milliseconds (default: 10000) */
|
|
14
|
+
timeout?: number;
|
|
15
|
+
/** Maximum retry attempts (default: 3) */
|
|
16
|
+
maxRetries?: number;
|
|
17
|
+
/** Delay between retries in milliseconds (default: 2000) */
|
|
18
|
+
retryDelay?: number;
|
|
19
|
+
/** Polling interval for checking API readiness in milliseconds (default: 100) */
|
|
20
|
+
pollInterval?: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* APILoader class handles loading external scripts with retry logic and deduplication.
|
|
25
|
+
* Multiple components can request the same API without duplicate loads.
|
|
26
|
+
*/
|
|
27
|
+
export class APILoader {
|
|
28
|
+
private static loadingPromises = new Map<string, Promise<void>>();
|
|
29
|
+
private static loadedScripts = new Set<string>();
|
|
30
|
+
private static readyApis = new Set<string>();
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Load an external API script with retry mechanism.
|
|
34
|
+
* @param key - Unique identifier for this API (e.g., 'youtube', 'vimeo')
|
|
35
|
+
* @param config - Configuration for loading the script
|
|
36
|
+
* @returns Promise that resolves when the API is ready
|
|
37
|
+
*/
|
|
38
|
+
static async loadScript(key: string, config: APILoaderConfig): Promise<void> {
|
|
39
|
+
const { maxRetries = 3, retryDelay = 2000 } = config;
|
|
40
|
+
|
|
41
|
+
return this.loadWithRetry(key, config, 0, maxRetries, retryDelay);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Internal method to load script with retry logic.
|
|
46
|
+
*/
|
|
47
|
+
private static async loadWithRetry(
|
|
48
|
+
key: string,
|
|
49
|
+
config: APILoaderConfig,
|
|
50
|
+
retries: number,
|
|
51
|
+
maxRetries: number,
|
|
52
|
+
retryDelay: number,
|
|
53
|
+
): Promise<void> {
|
|
54
|
+
try {
|
|
55
|
+
await this.loadScriptOnce(key, config);
|
|
56
|
+
} catch (error) {
|
|
57
|
+
if (retries < maxRetries) {
|
|
58
|
+
console.warn(
|
|
59
|
+
`${key} API load failed, retrying (${retries + 1}/${maxRetries})...`,
|
|
60
|
+
);
|
|
61
|
+
await new Promise((resolve) => setTimeout(resolve, retryDelay));
|
|
62
|
+
return this.loadWithRetry(
|
|
63
|
+
key,
|
|
64
|
+
config,
|
|
65
|
+
retries + 1,
|
|
66
|
+
maxRetries,
|
|
67
|
+
retryDelay,
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
throw error;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Internal method to load script once.
|
|
76
|
+
* Handles deduplication and manages loading state.
|
|
77
|
+
*/
|
|
78
|
+
private static async loadScriptOnce(
|
|
79
|
+
key: string,
|
|
80
|
+
config: APILoaderConfig,
|
|
81
|
+
): Promise<void> {
|
|
82
|
+
const {
|
|
83
|
+
scriptUrl,
|
|
84
|
+
globalCheck,
|
|
85
|
+
globalCallback,
|
|
86
|
+
timeout = 10000,
|
|
87
|
+
pollInterval = 100,
|
|
88
|
+
} = config;
|
|
89
|
+
|
|
90
|
+
// Already ready
|
|
91
|
+
if (this.readyApis.has(key)) {
|
|
92
|
+
return Promise.resolve();
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Already loading - wait for existing promise
|
|
96
|
+
if (this.loadingPromises.has(key)) {
|
|
97
|
+
return this.loadingPromises.get(key)!;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Create new loading promise
|
|
101
|
+
const promise = new Promise<void>((resolve, reject) => {
|
|
102
|
+
const timeoutId = setTimeout(() => {
|
|
103
|
+
cleanup();
|
|
104
|
+
this.loadingPromises.delete(key);
|
|
105
|
+
reject(
|
|
106
|
+
new Error(
|
|
107
|
+
`${key} API loading timeout. The API script may be blocked by an ad blocker or network issue.`,
|
|
108
|
+
),
|
|
109
|
+
);
|
|
110
|
+
}, timeout);
|
|
111
|
+
|
|
112
|
+
const cleanup = () => {
|
|
113
|
+
clearTimeout(timeoutId);
|
|
114
|
+
if (globalCallback) {
|
|
115
|
+
delete (window as any)[globalCallback];
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
// Add script if not already loaded
|
|
120
|
+
if (!this.loadedScripts.has(key)) {
|
|
121
|
+
const script = document.createElement("script");
|
|
122
|
+
script.src = scriptUrl;
|
|
123
|
+
script.onerror = () => {
|
|
124
|
+
cleanup();
|
|
125
|
+
this.loadingPromises.delete(key);
|
|
126
|
+
reject(
|
|
127
|
+
new Error(
|
|
128
|
+
`Failed to load ${key} API script. Please check your network connection or disable ad blockers.`,
|
|
129
|
+
),
|
|
130
|
+
);
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
// If there's a global callback (YouTube style)
|
|
134
|
+
if (globalCallback) {
|
|
135
|
+
(window as any)[globalCallback] = () => {
|
|
136
|
+
this.readyApis.add(key);
|
|
137
|
+
this.loadingPromises.delete(key);
|
|
138
|
+
cleanup();
|
|
139
|
+
resolve();
|
|
140
|
+
};
|
|
141
|
+
} else {
|
|
142
|
+
// Poll for readiness (Vimeo style)
|
|
143
|
+
script.onload = () => {
|
|
144
|
+
const checkReady = setInterval(() => {
|
|
145
|
+
if (globalCheck()) {
|
|
146
|
+
this.readyApis.add(key);
|
|
147
|
+
this.loadingPromises.delete(key);
|
|
148
|
+
clearInterval(checkReady);
|
|
149
|
+
cleanup();
|
|
150
|
+
resolve();
|
|
151
|
+
}
|
|
152
|
+
}, pollInterval);
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
document.head.appendChild(script);
|
|
157
|
+
this.loadedScripts.add(key);
|
|
158
|
+
} else if (!this.readyApis.has(key)) {
|
|
159
|
+
// Script already added but not ready yet - poll for readiness
|
|
160
|
+
const checkReady = setInterval(() => {
|
|
161
|
+
if (globalCheck()) {
|
|
162
|
+
this.readyApis.add(key);
|
|
163
|
+
this.loadingPromises.delete(key);
|
|
164
|
+
clearInterval(checkReady);
|
|
165
|
+
cleanup();
|
|
166
|
+
resolve();
|
|
167
|
+
}
|
|
168
|
+
}, pollInterval);
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
this.loadingPromises.set(key, promise);
|
|
173
|
+
return promise;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Check if an API is ready without loading it.
|
|
178
|
+
* Useful for testing or conditional logic.
|
|
179
|
+
*/
|
|
180
|
+
static isReady(key: string): boolean {
|
|
181
|
+
return this.readyApis.has(key);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Reset the loader state (mainly for testing).
|
|
186
|
+
*/
|
|
187
|
+
static reset(): void {
|
|
188
|
+
this.loadingPromises.clear();
|
|
189
|
+
this.loadedScripts.clear();
|
|
190
|
+
this.readyApis.clear();
|
|
191
|
+
}
|
|
192
|
+
}
|