@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
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Shared utility for loading external API scripts (YouTube, Vimeo, etc.)
3
+ * Provides retry mechanism, timeout handling, and deduplication.
4
+ */
5
+ export interface APILoaderConfig {
6
+ /** URL of the script to load */
7
+ scriptUrl: string;
8
+ /** Function to check if the API is ready (e.g., checking global variables) */
9
+ globalCheck: () => boolean;
10
+ /** Optional global callback function name (e.g., 'onYouTubeIframeAPIReady') */
11
+ globalCallback?: string;
12
+ /** Timeout in milliseconds (default: 10000) */
13
+ timeout?: number;
14
+ /** Maximum retry attempts (default: 3) */
15
+ maxRetries?: number;
16
+ /** Delay between retries in milliseconds (default: 2000) */
17
+ retryDelay?: number;
18
+ /** Polling interval for checking API readiness in milliseconds (default: 100) */
19
+ pollInterval?: number;
20
+ }
21
+ /**
22
+ * APILoader class handles loading external scripts with retry logic and deduplication.
23
+ * Multiple components can request the same API without duplicate loads.
24
+ */
25
+ export declare class APILoader {
26
+ private static loadingPromises;
27
+ private static loadedScripts;
28
+ private static readyApis;
29
+ /**
30
+ * Load an external API script with retry mechanism.
31
+ * @param key - Unique identifier for this API (e.g., 'youtube', 'vimeo')
32
+ * @param config - Configuration for loading the script
33
+ * @returns Promise that resolves when the API is ready
34
+ */
35
+ static loadScript(key: string, config: APILoaderConfig): Promise<void>;
36
+ /**
37
+ * Internal method to load script with retry logic.
38
+ */
39
+ private static loadWithRetry;
40
+ /**
41
+ * Internal method to load script once.
42
+ * Handles deduplication and manages loading state.
43
+ */
44
+ private static loadScriptOnce;
45
+ /**
46
+ * Check if an API is ready without loading it.
47
+ * Useful for testing or conditional logic.
48
+ */
49
+ static isReady(key: string): boolean;
50
+ /**
51
+ * Reset the loader state (mainly for testing).
52
+ */
53
+ static reset(): void;
54
+ }
55
+ //# sourceMappingURL=APILoader.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"APILoader.d.ts","sourceRoot":"","sources":["../../src/utils/APILoader.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,MAAM,WAAW,eAAe;IAC9B,gCAAgC;IAChC,SAAS,EAAE,MAAM,CAAC;IAClB,8EAA8E;IAC9E,WAAW,EAAE,MAAM,OAAO,CAAC;IAC3B,+EAA+E;IAC/E,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,+CAA+C;IAC/C,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,0CAA0C;IAC1C,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,4DAA4D;IAC5D,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,iFAAiF;IACjF,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED;;;GAGG;AACH,qBAAa,SAAS;IACpB,OAAO,CAAC,MAAM,CAAC,eAAe,CAAoC;IAClE,OAAO,CAAC,MAAM,CAAC,aAAa,CAAqB;IACjD,OAAO,CAAC,MAAM,CAAC,SAAS,CAAqB;IAE7C;;;;;OAKG;WACU,UAAU,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC;IAM5E;;OAEG;mBACkB,aAAa;IA2BlC;;;OAGG;mBACkB,cAAc;IAkGnC;;;OAGG;IACH,MAAM,CAAC,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO;IAIpC;;OAEG;IACH,MAAM,CAAC,KAAK,IAAI,IAAI;CAKrB"}
@@ -0,0 +1,148 @@
1
+ /**
2
+ * Shared utility for loading external API scripts (YouTube, Vimeo, etc.)
3
+ * Provides retry mechanism, timeout handling, and deduplication.
4
+ */
5
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
6
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
7
+ return new (P || (P = Promise))(function (resolve, reject) {
8
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
9
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
10
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
11
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
12
+ });
13
+ };
14
+ /**
15
+ * APILoader class handles loading external scripts with retry logic and deduplication.
16
+ * Multiple components can request the same API without duplicate loads.
17
+ */
18
+ export class APILoader {
19
+ /**
20
+ * Load an external API script with retry mechanism.
21
+ * @param key - Unique identifier for this API (e.g., 'youtube', 'vimeo')
22
+ * @param config - Configuration for loading the script
23
+ * @returns Promise that resolves when the API is ready
24
+ */
25
+ static loadScript(key, config) {
26
+ return __awaiter(this, void 0, void 0, function* () {
27
+ const { maxRetries = 3, retryDelay = 2000 } = config;
28
+ return this.loadWithRetry(key, config, 0, maxRetries, retryDelay);
29
+ });
30
+ }
31
+ /**
32
+ * Internal method to load script with retry logic.
33
+ */
34
+ static loadWithRetry(key, config, retries, maxRetries, retryDelay) {
35
+ return __awaiter(this, void 0, void 0, function* () {
36
+ try {
37
+ yield this.loadScriptOnce(key, config);
38
+ }
39
+ catch (error) {
40
+ if (retries < maxRetries) {
41
+ console.warn(`${key} API load failed, retrying (${retries + 1}/${maxRetries})...`);
42
+ yield new Promise((resolve) => setTimeout(resolve, retryDelay));
43
+ return this.loadWithRetry(key, config, retries + 1, maxRetries, retryDelay);
44
+ }
45
+ throw error;
46
+ }
47
+ });
48
+ }
49
+ /**
50
+ * Internal method to load script once.
51
+ * Handles deduplication and manages loading state.
52
+ */
53
+ static loadScriptOnce(key, config) {
54
+ return __awaiter(this, void 0, void 0, function* () {
55
+ const { scriptUrl, globalCheck, globalCallback, timeout = 10000, pollInterval = 100, } = config;
56
+ // Already ready
57
+ if (this.readyApis.has(key)) {
58
+ return Promise.resolve();
59
+ }
60
+ // Already loading - wait for existing promise
61
+ if (this.loadingPromises.has(key)) {
62
+ return this.loadingPromises.get(key);
63
+ }
64
+ // Create new loading promise
65
+ const promise = new Promise((resolve, reject) => {
66
+ const timeoutId = setTimeout(() => {
67
+ cleanup();
68
+ this.loadingPromises.delete(key);
69
+ reject(new Error(`${key} API loading timeout. The API script may be blocked by an ad blocker or network issue.`));
70
+ }, timeout);
71
+ const cleanup = () => {
72
+ clearTimeout(timeoutId);
73
+ if (globalCallback) {
74
+ delete window[globalCallback];
75
+ }
76
+ };
77
+ // Add script if not already loaded
78
+ if (!this.loadedScripts.has(key)) {
79
+ const script = document.createElement("script");
80
+ script.src = scriptUrl;
81
+ script.onerror = () => {
82
+ cleanup();
83
+ this.loadingPromises.delete(key);
84
+ reject(new Error(`Failed to load ${key} API script. Please check your network connection or disable ad blockers.`));
85
+ };
86
+ // If there's a global callback (YouTube style)
87
+ if (globalCallback) {
88
+ window[globalCallback] = () => {
89
+ this.readyApis.add(key);
90
+ this.loadingPromises.delete(key);
91
+ cleanup();
92
+ resolve();
93
+ };
94
+ }
95
+ else {
96
+ // Poll for readiness (Vimeo style)
97
+ script.onload = () => {
98
+ const checkReady = setInterval(() => {
99
+ if (globalCheck()) {
100
+ this.readyApis.add(key);
101
+ this.loadingPromises.delete(key);
102
+ clearInterval(checkReady);
103
+ cleanup();
104
+ resolve();
105
+ }
106
+ }, pollInterval);
107
+ };
108
+ }
109
+ document.head.appendChild(script);
110
+ this.loadedScripts.add(key);
111
+ }
112
+ else if (!this.readyApis.has(key)) {
113
+ // Script already added but not ready yet - poll for readiness
114
+ const checkReady = setInterval(() => {
115
+ if (globalCheck()) {
116
+ this.readyApis.add(key);
117
+ this.loadingPromises.delete(key);
118
+ clearInterval(checkReady);
119
+ cleanup();
120
+ resolve();
121
+ }
122
+ }, pollInterval);
123
+ }
124
+ });
125
+ this.loadingPromises.set(key, promise);
126
+ return promise;
127
+ });
128
+ }
129
+ /**
130
+ * Check if an API is ready without loading it.
131
+ * Useful for testing or conditional logic.
132
+ */
133
+ static isReady(key) {
134
+ return this.readyApis.has(key);
135
+ }
136
+ /**
137
+ * Reset the loader state (mainly for testing).
138
+ */
139
+ static reset() {
140
+ this.loadingPromises.clear();
141
+ this.loadedScripts.clear();
142
+ this.readyApis.clear();
143
+ }
144
+ }
145
+ APILoader.loadingPromises = new Map();
146
+ APILoader.loadedScripts = new Set();
147
+ APILoader.readyApis = new Set();
148
+ //# sourceMappingURL=APILoader.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"APILoader.js","sourceRoot":"","sources":["../../src/utils/APILoader.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@smartimpact-it/modern-video-embed",
3
- "version": "2.0.6",
3
+ "version": "2.0.8",
4
4
  "description": "Modern YouTube and Vimeo embed web components with lazy loading, accessibility, quality control, and comprehensive API",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.js",
@@ -35,6 +35,15 @@ export abstract class BaseVideoEmbed extends HTMLElement {
35
35
  protected abstract reinitializePlayer(): void;
36
36
  protected abstract destroyPlayer(): void;
37
37
  protected abstract getComponentName(): string;
38
+ protected abstract handlePlay(): void;
39
+ protected abstract handlePause(): void;
40
+
41
+ /**
42
+ * Abstract methods for component-specific attribute handling
43
+ */
44
+ protected abstract syncMutedState(): void;
45
+ protected abstract syncControlsState(): void;
46
+ protected abstract syncPosterState(): void;
38
47
 
39
48
  /**
40
49
  * Logging utilities
@@ -89,6 +98,72 @@ export abstract class BaseVideoEmbed extends HTMLElement {
89
98
  this.updatingAttribute = false;
90
99
  }
91
100
 
101
+ /**
102
+ * Common attribute change handler for attributes shared across all components.
103
+ * Returns true if the attribute was handled, false otherwise.
104
+ * Subclasses should call this first, then handle component-specific attributes.
105
+ */
106
+ protected handleCommonAttributeChange(
107
+ name: string,
108
+ _oldValue: string | null,
109
+ newValue: string | null,
110
+ ): boolean {
111
+ switch (name) {
112
+ case "autoplay":
113
+ this._autoplay = newValue !== null;
114
+ if (
115
+ !this._playing &&
116
+ this._autoplay &&
117
+ !this._lazy &&
118
+ this.initialized
119
+ ) {
120
+ this.play();
121
+ }
122
+ return true;
123
+
124
+ case "lazy":
125
+ this._lazy = newValue !== null;
126
+ return true;
127
+
128
+ case "muted":
129
+ this._muted = newValue !== null;
130
+ this.syncMutedState();
131
+ return true;
132
+
133
+ case "controls":
134
+ this._controls = newValue !== null;
135
+ this.syncControlsState();
136
+ return true;
137
+
138
+ case "poster":
139
+ this._poster = newValue || "";
140
+ this.syncPosterState();
141
+ return true;
142
+
143
+ case "background":
144
+ this._background = newValue !== null;
145
+ this.updateBackgroundMode();
146
+ return true;
147
+
148
+ default:
149
+ return false; // Not handled by base class
150
+ }
151
+ }
152
+
153
+ /**
154
+ * Public play method - subclasses can override
155
+ */
156
+ public play(): void {
157
+ this.handlePlay();
158
+ }
159
+
160
+ /**
161
+ * Public pause method - subclasses can override
162
+ */
163
+ public pause(): void {
164
+ this.handlePause();
165
+ }
166
+
92
167
  /**
93
168
  * Error message display (common across all components)
94
169
  */
@@ -148,6 +223,38 @@ export abstract class BaseVideoEmbed extends HTMLElement {
148
223
  }
149
224
  }
150
225
 
226
+ /**
227
+ * Automatically set aspect ratio CSS custom properties
228
+ * This allows background videos to maintain correct proportions
229
+ */
230
+ protected setAspectRatio(width: number, height: number): void {
231
+ if (width > 0 && height > 0) {
232
+ // Calculate GCD for simplest ratio
233
+ const gcd = (a: number, b: number): number =>
234
+ b === 0 ? a : gcd(b, a % b);
235
+ const divisor = gcd(width, height);
236
+ const aspectWidth = width / divisor;
237
+ const aspectHeight = height / divisor;
238
+
239
+ this.style.setProperty("--si-embed-aspect-width", aspectWidth.toString());
240
+ this.style.setProperty(
241
+ "--si-embed-aspect-height",
242
+ aspectHeight.toString(),
243
+ );
244
+
245
+ // Also update the padding-top percentage for normal mode
246
+ const aspectRatioPercent = (height / width) * 100;
247
+ this.style.setProperty(
248
+ "--si-embed-video-aspect-ratio",
249
+ `${aspectRatioPercent}%`,
250
+ );
251
+
252
+ this.log(
253
+ `Aspect ratio auto-detected: ${aspectWidth}:${aspectHeight} (${width}x${height})`,
254
+ );
255
+ }
256
+ }
257
+
151
258
  /**
152
259
  * Custom controls creation (similar pattern across components)
153
260
  */
@@ -189,12 +296,6 @@ export abstract class BaseVideoEmbed extends HTMLElement {
189
296
  this.appendChild(buttonOverlay);
190
297
  }
191
298
 
192
- /**
193
- * Play/Pause handlers - subclasses override with platform-specific logic
194
- */
195
- protected abstract handlePlay(): void;
196
- protected abstract handlePause(): void;
197
-
198
299
  /**
199
300
  * Lazy loading setup (common pattern)
200
301
  */
@@ -37,6 +37,12 @@ export class VideoEmbed extends BaseVideoEmbed {
37
37
 
38
38
  this.log(`Attribute changed - ${name}: ${oldValue} -> ${newValue}`);
39
39
 
40
+ // Try common attribute handling first
41
+ if (this.handleCommonAttributeChange(name, oldValue, newValue)) {
42
+ return;
43
+ }
44
+
45
+ // Handle VideoEmbed-specific attributes
40
46
  switch (name) {
41
47
  case "url":
42
48
  this.#url = newValue || "";
@@ -44,44 +50,6 @@ export class VideoEmbed extends BaseVideoEmbed {
44
50
  this.reinitializePlayer();
45
51
  }
46
52
  break;
47
- case "autoplay":
48
- this._autoplay = newValue !== null;
49
- if (
50
- !this._playing &&
51
- this._autoplay &&
52
- !this._lazy &&
53
- this.initialized
54
- ) {
55
- this.play();
56
- }
57
- break;
58
- case "controls":
59
- this._controls = newValue !== null;
60
- if (this.video) {
61
- this.video.controls = this._controls;
62
- }
63
- break;
64
- case "lazy":
65
- this._lazy = newValue !== null;
66
- break;
67
- case "muted":
68
- this._muted = newValue !== null;
69
- if (this.video) {
70
- this.video.muted = this._muted;
71
- }
72
- break;
73
- case "poster":
74
- this._poster = newValue || "";
75
- if (this._lazy && !this.video) {
76
- this.showPoster();
77
- } else if (this.video) {
78
- this.video.poster = this._poster;
79
- }
80
- break;
81
- case "background":
82
- this._background = newValue !== null;
83
- this.updateBackgroundMode();
84
- break;
85
53
  case "loop":
86
54
  this.#loop = newValue !== null;
87
55
  if (this.video) {
@@ -133,6 +101,11 @@ export class VideoEmbed extends BaseVideoEmbed {
133
101
  return this._controls;
134
102
  }
135
103
  set controls(value: boolean) {
104
+ // Background mode videos must not have controls
105
+ if (this._background && value) {
106
+ this.warn("Cannot enable controls on background video");
107
+ return;
108
+ }
136
109
  this._controls = value;
137
110
  this.reflectBooleanAttribute("controls", value);
138
111
  if (this.video) {
@@ -152,6 +125,11 @@ export class VideoEmbed extends BaseVideoEmbed {
152
125
  return this._muted;
153
126
  }
154
127
  set muted(value: boolean) {
128
+ // Background mode videos must be muted
129
+ if (this._background && !value) {
130
+ this.warn("Cannot unmute background video");
131
+ return;
132
+ }
155
133
  this._muted = value;
156
134
  this.reflectBooleanAttribute("muted", value);
157
135
  if (this.video) {
@@ -489,6 +467,29 @@ export class VideoEmbed extends BaseVideoEmbed {
489
467
  }
490
468
  }
491
469
 
470
+ /**
471
+ * Sync methods for common attributes (required by BaseVideoEmbed)
472
+ */
473
+ protected syncMutedState(): void {
474
+ if (this.video) {
475
+ this.video.muted = this._muted;
476
+ }
477
+ }
478
+
479
+ protected syncControlsState(): void {
480
+ if (this.video) {
481
+ this.video.controls = this._controls;
482
+ }
483
+ }
484
+
485
+ protected syncPosterState(): void {
486
+ if (this._lazy && !this.video) {
487
+ this.showPoster();
488
+ } else if (this.video) {
489
+ this.video.poster = this._poster;
490
+ }
491
+ }
492
+
492
493
  private setupVideoEvents() {
493
494
  if (!this.video) return;
494
495
 
@@ -556,10 +557,18 @@ export class VideoEmbed extends BaseVideoEmbed {
556
557
  });
557
558
 
558
559
  this.video.addEventListener("loadedmetadata", () => {
560
+ const width = this.video?.videoWidth || 0;
561
+ const height = this.video?.videoHeight || 0;
562
+
563
+ // Automatically set aspect ratio CSS custom properties
564
+ if (width > 0 && height > 0) {
565
+ this.setAspectRatio(width, height);
566
+ }
567
+
559
568
  this.dispatchCustomEvent("loadedmetadata", {
560
569
  duration: this.video?.duration || 0,
561
- videoWidth: this.video?.videoWidth || 0,
562
- videoHeight: this.video?.videoHeight || 0,
570
+ videoWidth: width,
571
+ videoHeight: height,
563
572
  });
564
573
  });
565
574
  }
@@ -657,7 +666,7 @@ export class VideoEmbed extends BaseVideoEmbed {
657
666
  }
658
667
 
659
668
  // Public API methods
660
- public async play(): Promise<void> {
669
+ public override async play(): Promise<void> {
661
670
  if (this._lazy && !this.video) {
662
671
  this.log("Lazy video needs to be loaded first. Initializing...");
663
672
  this.setAttribute("data-should-autoplay", "true");
@@ -681,7 +690,7 @@ export class VideoEmbed extends BaseVideoEmbed {
681
690
  }
682
691
  }
683
692
 
684
- public pause(): void {
693
+ public override pause(): void {
685
694
  if (this.video) {
686
695
  this.video.pause();
687
696
  this.log("Video paused.");