@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.
Files changed (36) hide show
  1. package/dist/components/BaseVideoEmbed.d.ts +27 -5
  2. package/dist/components/BaseVideoEmbed.d.ts.map +1 -1
  3. package/dist/components/BaseVideoEmbed.js +70 -0
  4. package/dist/components/BaseVideoEmbed.js.map +1 -1
  5. package/dist/components/VideoEmbed.d.ts +6 -0
  6. package/dist/components/VideoEmbed.d.ts.map +1 -1
  7. package/dist/components/VideoEmbed.js +45 -40
  8. package/dist/components/VideoEmbed.js.map +1 -1
  9. package/dist/components/VimeoEmbed.d.ts +7 -5
  10. package/dist/components/VimeoEmbed.d.ts.map +1 -1
  11. package/dist/components/VimeoEmbed.js +67 -97
  12. package/dist/components/VimeoEmbed.js.map +1 -1
  13. package/dist/components/VimeoEmbed.min.js +1 -1
  14. package/dist/components/YouTubeEmbed.d.ts +7 -6
  15. package/dist/components/YouTubeEmbed.d.ts.map +1 -1
  16. package/dist/components/YouTubeEmbed.js +65 -108
  17. package/dist/components/YouTubeEmbed.js.map +1 -1
  18. package/dist/components/YouTubeEmbed.min.js +1 -1
  19. package/dist/css/components.css +68 -42
  20. package/dist/css/components.css.map +1 -1
  21. package/dist/css/components.min.css +1 -1
  22. package/dist/css/main.css +68 -42
  23. package/dist/css/main.css.map +1 -1
  24. package/dist/css/main.min.css +1 -1
  25. package/dist/utils/APILoader.d.ts +55 -0
  26. package/dist/utils/APILoader.d.ts.map +1 -0
  27. package/dist/utils/APILoader.js +148 -0
  28. package/dist/utils/APILoader.js.map +1 -0
  29. package/package.json +1 -1
  30. package/src/components/BaseVideoEmbed.ts +107 -6
  31. package/src/components/VideoEmbed.ts +51 -42
  32. package/src/components/VimeoEmbed.ts +71 -113
  33. package/src/components/YouTubeEmbed.ts +71 -130
  34. package/src/styles/_embed-base.scss +35 -15
  35. package/src/styles/video-embed.scss +18 -0
  36. 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
- --poster-object-fit: cover;
12
- --video-object-fit: contain;
13
- --control-button-size: 70px;
14
- --control-button-color: #ffffff;
15
- --overlay-background-color: rgba(0, 0, 0, 0.5);
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: 16 / 9;
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: 100%;
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
+ }