@smartimpact-it/modern-video-embed 2.0.6 → 2.0.7

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/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.7",
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",
@@ -148,6 +148,38 @@ export abstract class BaseVideoEmbed extends HTMLElement {
148
148
  }
149
149
  }
150
150
 
151
+ /**
152
+ * Automatically set aspect ratio CSS custom properties
153
+ * This allows background videos to maintain correct proportions
154
+ */
155
+ protected setAspectRatio(width: number, height: number): void {
156
+ if (width > 0 && height > 0) {
157
+ // Calculate GCD for simplest ratio
158
+ const gcd = (a: number, b: number): number =>
159
+ b === 0 ? a : gcd(b, a % b);
160
+ const divisor = gcd(width, height);
161
+ const aspectWidth = width / divisor;
162
+ const aspectHeight = height / divisor;
163
+
164
+ this.style.setProperty("--si-embed-aspect-width", aspectWidth.toString());
165
+ this.style.setProperty(
166
+ "--si-embed-aspect-height",
167
+ aspectHeight.toString(),
168
+ );
169
+
170
+ // Also update the padding-top percentage for normal mode
171
+ const aspectRatioPercent = (height / width) * 100;
172
+ this.style.setProperty(
173
+ "--si-embed-video-aspect-ratio",
174
+ `${aspectRatioPercent}%`,
175
+ );
176
+
177
+ this.log(
178
+ `Aspect ratio auto-detected: ${aspectWidth}:${aspectHeight} (${width}x${height})`,
179
+ );
180
+ }
181
+ }
182
+
151
183
  /**
152
184
  * Custom controls creation (similar pattern across components)
153
185
  */
@@ -133,6 +133,11 @@ export class VideoEmbed extends BaseVideoEmbed {
133
133
  return this._controls;
134
134
  }
135
135
  set controls(value: boolean) {
136
+ // Background mode videos must not have controls
137
+ if (this._background && value) {
138
+ this.warn("Cannot enable controls on background video");
139
+ return;
140
+ }
136
141
  this._controls = value;
137
142
  this.reflectBooleanAttribute("controls", value);
138
143
  if (this.video) {
@@ -152,6 +157,11 @@ export class VideoEmbed extends BaseVideoEmbed {
152
157
  return this._muted;
153
158
  }
154
159
  set muted(value: boolean) {
160
+ // Background mode videos must be muted
161
+ if (this._background && !value) {
162
+ this.warn("Cannot unmute background video");
163
+ return;
164
+ }
155
165
  this._muted = value;
156
166
  this.reflectBooleanAttribute("muted", value);
157
167
  if (this.video) {
@@ -556,10 +566,18 @@ export class VideoEmbed extends BaseVideoEmbed {
556
566
  });
557
567
 
558
568
  this.video.addEventListener("loadedmetadata", () => {
569
+ const width = this.video?.videoWidth || 0;
570
+ const height = this.video?.videoHeight || 0;
571
+
572
+ // Automatically set aspect ratio CSS custom properties
573
+ if (width > 0 && height > 0) {
574
+ this.setAspectRatio(width, height);
575
+ }
576
+
559
577
  this.dispatchCustomEvent("loadedmetadata", {
560
578
  duration: this.video?.duration || 0,
561
- videoWidth: this.video?.videoWidth || 0,
562
- videoHeight: this.video?.videoHeight || 0,
579
+ videoWidth: width,
580
+ videoHeight: height,
563
581
  });
564
582
  });
565
583
  }
@@ -152,6 +152,11 @@ export class VimeoEmbed extends BaseVideoEmbed {
152
152
  return this._controls;
153
153
  }
154
154
  set controls(value: boolean) {
155
+ // Background mode videos must not have controls
156
+ if (this._background && value) {
157
+ this.warn("Cannot enable controls on background video");
158
+ return;
159
+ }
155
160
  this._controls = value;
156
161
  this.reflectBooleanAttribute("controls", value);
157
162
  if (this.initialized) {
@@ -171,6 +176,11 @@ export class VimeoEmbed extends BaseVideoEmbed {
171
176
  return this._muted;
172
177
  }
173
178
  set muted(value: boolean) {
179
+ // Background mode videos must be muted
180
+ if (this._background && !value) {
181
+ this.warn("Cannot unmute background video");
182
+ return;
183
+ }
174
184
  this._muted = value;
175
185
  this.reflectBooleanAttribute("muted", value);
176
186
  if (this.player && this.playerReady) {
@@ -639,13 +649,17 @@ export class VimeoEmbed extends BaseVideoEmbed {
639
649
  this.iframe.allow =
640
650
  "autoplay; fullscreen; picture-in-picture; clipboard-write";
641
651
  this.iframe.allowFullscreen = true;
642
- // Set responsive sizing
643
- this.iframe.style.width = "100%";
644
- this.iframe.style.height = "100%";
645
652
  this.iframe.style.border = "none";
646
653
 
654
+ // Set responsive sizing based on mode
647
655
  if (this._background) {
656
+ // In background mode, let CSS handle sizing with aspect-ratio
648
657
  this.iframe.style.pointerEvents = "none";
658
+ // Don't set width/height inline to allow CSS aspect-ratio to work
659
+ } else {
660
+ // In normal mode, fill the container
661
+ this.iframe.style.width = "100%";
662
+ this.iframe.style.height = "100%";
649
663
  }
650
664
 
651
665
  // Add tabindex for keyboard accessibility
@@ -683,7 +697,7 @@ export class VimeoEmbed extends BaseVideoEmbed {
683
697
  // Use the iframe element directly since it's already in the DOM
684
698
  this.player = new Vimeo.Player(iframe);
685
699
 
686
- this.player.on("loaded", () => {
700
+ this.player.on("loaded", async () => {
687
701
  this.log("VimeoEmbed: Player is ready.");
688
702
  this.playerReady = true;
689
703
 
@@ -691,6 +705,19 @@ export class VimeoEmbed extends BaseVideoEmbed {
691
705
  this.player.setVolume(0);
692
706
  }
693
707
 
708
+ // Get video dimensions for aspect ratio
709
+ try {
710
+ const width = await this.player.getVideoWidth();
711
+ const height = await this.player.getVideoHeight();
712
+ if (width > 0 && height > 0) {
713
+ this.setAspectRatio(width, height);
714
+ }
715
+ } catch (error) {
716
+ this.warn("Could not get video dimensions:", error);
717
+ // Default to 16:9 if dimensions not available
718
+ this.setAspectRatio(16, 9);
719
+ }
720
+
694
721
  if (autoplay && this.setCustomControlState) {
695
722
  this.setCustomControlState(true);
696
723
  this._playing = true;
@@ -174,6 +174,11 @@ export class YouTubeEmbed extends BaseVideoEmbed {
174
174
  return this._controls;
175
175
  }
176
176
  set controls(value: boolean) {
177
+ // Background mode videos must not have controls
178
+ if (this._background && value) {
179
+ this.warn("Cannot enable controls on background video");
180
+ return;
181
+ }
177
182
  this._controls = value;
178
183
  this.reflectBooleanAttribute("controls", value);
179
184
  if (this.initialized) {
@@ -200,6 +205,11 @@ export class YouTubeEmbed extends BaseVideoEmbed {
200
205
  return this._muted;
201
206
  }
202
207
  set muted(value: boolean) {
208
+ // Background mode videos must be muted
209
+ if (this._background && !value) {
210
+ this.warn("Cannot unmute background video");
211
+ return;
212
+ }
203
213
  this._muted = value;
204
214
  this.reflectBooleanAttribute("muted", value);
205
215
  if (this.player && this.playerReady) {
@@ -737,15 +747,22 @@ export class YouTubeEmbed extends BaseVideoEmbed {
737
747
  // Get the iframe element that YT.Player created
738
748
  this.iframe = this.player.getIframe();
739
749
 
740
- // Apply responsive styling to iframe
741
- if (this.iframe) {
742
- this.iframe.style.width = "100%";
743
- this.iframe.style.height = "100%";
744
- }
750
+ // Set aspect ratio (YouTube is typically 16:9)
751
+ this.setAspectRatio(16, 9);
745
752
 
746
- // Apply background mode styling if needed
747
- if (this._background && this.iframe) {
748
- this.iframe.style.pointerEvents = "none";
753
+ // Apply styling to iframe
754
+ if (this.iframe) {
755
+ if (this._background) {
756
+ // In background mode, let CSS handle sizing with aspect-ratio
757
+ this.iframe.style.pointerEvents = "none";
758
+ // Remove any inline sizing to let CSS aspect-ratio work
759
+ this.iframe.style.removeProperty("width");
760
+ this.iframe.style.removeProperty("height");
761
+ } else {
762
+ // In normal mode, fill the container
763
+ this.iframe.style.width = "100%";
764
+ this.iframe.style.height = "100%";
765
+ }
749
766
  }
750
767
 
751
768
  // Sync muted state with YouTube player
@@ -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)