@smartimpact-it/modern-video-embed 2.0.1

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 (51) hide show
  1. package/README.md +1205 -0
  2. package/dist/components/VimeoEmbed.d.ts +143 -0
  3. package/dist/components/VimeoEmbed.d.ts.map +1 -0
  4. package/dist/components/VimeoEmbed.js +1176 -0
  5. package/dist/components/VimeoEmbed.js.map +1 -0
  6. package/dist/components/VimeoEmbed.min.js +1 -0
  7. package/dist/components/YouTubeEmbed.d.ts +225 -0
  8. package/dist/components/YouTubeEmbed.d.ts.map +1 -0
  9. package/dist/components/YouTubeEmbed.js +1354 -0
  10. package/dist/components/YouTubeEmbed.js.map +1 -0
  11. package/dist/components/YouTubeEmbed.min.js +1 -0
  12. package/dist/css/components.css +349 -0
  13. package/dist/css/components.css.map +1 -0
  14. package/dist/css/components.min.css +1 -0
  15. package/dist/css/main.css +12210 -0
  16. package/dist/css/main.css.map +1 -0
  17. package/dist/css/main.min.css +7 -0
  18. package/dist/index.d.ts +3 -0
  19. package/dist/index.d.ts.map +1 -0
  20. package/dist/index.js +4 -0
  21. package/dist/index.js.map +1 -0
  22. package/dist/index.min.js +1 -0
  23. package/dist/types/index.d.ts +7 -0
  24. package/dist/types/index.d.ts.map +1 -0
  25. package/dist/types/index.js +5 -0
  26. package/dist/types/index.js.map +1 -0
  27. package/dist/vimeo-only.d.ts +7 -0
  28. package/dist/vimeo-only.d.ts.map +1 -0
  29. package/dist/vimeo-only.js +8 -0
  30. package/dist/vimeo-only.js.map +1 -0
  31. package/dist/vimeo-only.min.js +1 -0
  32. package/dist/youtube-only.d.ts +7 -0
  33. package/dist/youtube-only.d.ts.map +1 -0
  34. package/dist/youtube-only.js +8 -0
  35. package/dist/youtube-only.js.map +1 -0
  36. package/dist/youtube-only.min.js +1 -0
  37. package/package.json +75 -0
  38. package/src/components/VimeoEmbed.ts +1340 -0
  39. package/src/components/YouTubeEmbed.ts +1568 -0
  40. package/src/index.ts +3 -0
  41. package/src/styles/README.md +56 -0
  42. package/src/styles/components.scss +7 -0
  43. package/src/styles/main.scss +10 -0
  44. package/src/styles/vimeo-embed.scss +255 -0
  45. package/src/styles/youtube-embed.scss +261 -0
  46. package/src/types/common.d.ts +198 -0
  47. package/src/types/index.ts +7 -0
  48. package/src/types/vimeo-embed.d.ts +80 -0
  49. package/src/types/youtube-embed.d.ts +83 -0
  50. package/src/vimeo-only.ts +9 -0
  51. package/src/youtube-only.ts +9 -0
@@ -0,0 +1,1354 @@
1
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
2
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
3
+ return new (P || (P = Promise))(function (resolve, reject) {
4
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
5
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
6
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
7
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
8
+ });
9
+ };
10
+ var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {
11
+ if (kind === "m") throw new TypeError("Private method is not writable");
12
+ if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
13
+ if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it");
14
+ return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
15
+ };
16
+ var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
17
+ if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
18
+ if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
19
+ return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
20
+ };
21
+ var _YouTubeEmbed_instances, _YouTubeEmbed_url, _YouTubeEmbed_videoId, _YouTubeEmbed_autoplay, _YouTubeEmbed_muted, _YouTubeEmbed_controls, _YouTubeEmbed_lazy, _YouTubeEmbed_poster, _YouTubeEmbed_playing, _YouTubeEmbed_background, _YouTubeEmbed_playerVars, _YouTubeEmbed_reflectBooleanAttribute, _YouTubeEmbed_updateBackgroundMode;
22
+ export class YouTubeEmbed extends HTMLElement {
23
+ constructor() {
24
+ super(...arguments);
25
+ _YouTubeEmbed_instances.add(this);
26
+ this.iframe = null;
27
+ this.player = null;
28
+ this.playerReady = false;
29
+ this.playPauseButton = null;
30
+ this.initialized = false;
31
+ this.setCustomControlState = null;
32
+ this.updatingAttribute = false; // Flag to prevent recursive attribute updates
33
+ this.updatingMutedState = false; // Flag to prevent sync from overwriting programmatic mute changes
34
+ /** The full YouTube URL (e.g., "https://www.youtube.com/watch?v=...") */
35
+ _YouTubeEmbed_url.set(this, "");
36
+ /** The 11-character YouTube video ID. */
37
+ _YouTubeEmbed_videoId.set(this, "");
38
+ /** Whether the video should autoplay when loaded. Requires `muted` to be true. */
39
+ _YouTubeEmbed_autoplay.set(this, false);
40
+ /** Whether the video audio is muted. */
41
+ _YouTubeEmbed_muted.set(this, false);
42
+ /** Whether to show the native YouTube player controls. */
43
+ _YouTubeEmbed_controls.set(this, false);
44
+ /** Whether to lazy-load the video, showing a poster image until interaction. */
45
+ _YouTubeEmbed_lazy.set(this, false);
46
+ /** URL for a custom poster image. Defaults to YouTube's thumbnail. */
47
+ _YouTubeEmbed_poster.set(this, "");
48
+ /** Read-only property to check if the video is currently playing. */
49
+ _YouTubeEmbed_playing.set(this, false);
50
+ /** Whether the video should act as a background, covering its container. */
51
+ _YouTubeEmbed_background.set(this, false);
52
+ /** Extra parameters to pass to the YouTube player. */
53
+ _YouTubeEmbed_playerVars.set(this, {});
54
+ // Declare the event listener properties
55
+ this.posterClickHandler = null;
56
+ this.keyboardHandler = null;
57
+ this.ariaLiveRegion = null;
58
+ this.apiLoadRetries = 0;
59
+ this.intersectionObserver = null;
60
+ this.hasLoadedVideo = false;
61
+ }
62
+ static get observedAttributes() {
63
+ return [
64
+ "url",
65
+ "video-id",
66
+ "autoplay",
67
+ "controls",
68
+ "lazy",
69
+ "muted",
70
+ "poster",
71
+ "background",
72
+ "player-vars",
73
+ "quality",
74
+ ];
75
+ }
76
+ attributeChangedCallback(name, oldValue, newValue) {
77
+ // Skip processing if we're currently updating an attribute to prevent infinite loops
78
+ if (this.updatingAttribute) {
79
+ return;
80
+ }
81
+ this.log(`YouTubeEmbed: Attribute changed - ${name}: ${oldValue} -> ${newValue}`);
82
+ switch (name) {
83
+ case "url":
84
+ __classPrivateFieldSet(this, _YouTubeEmbed_url, newValue || "", "f");
85
+ __classPrivateFieldSet(this, _YouTubeEmbed_videoId, this.extractVideoId(__classPrivateFieldGet(this, _YouTubeEmbed_url, "f")), "f");
86
+ if (this.initialized) {
87
+ this.reinitializePlayer();
88
+ }
89
+ break;
90
+ case "video-id":
91
+ __classPrivateFieldSet(this, _YouTubeEmbed_videoId, newValue || "", "f");
92
+ if (this.initialized) {
93
+ this.reinitializePlayer();
94
+ }
95
+ break;
96
+ case "autoplay":
97
+ __classPrivateFieldSet(this, _YouTubeEmbed_autoplay, newValue !== null, "f");
98
+ if (!__classPrivateFieldGet(this, _YouTubeEmbed_playing, "f") &&
99
+ __classPrivateFieldGet(this, _YouTubeEmbed_autoplay, "f") &&
100
+ !__classPrivateFieldGet(this, _YouTubeEmbed_lazy, "f") &&
101
+ this.initialized) {
102
+ this.play();
103
+ }
104
+ break;
105
+ case "controls":
106
+ __classPrivateFieldSet(this, _YouTubeEmbed_controls, newValue !== null, "f");
107
+ if (this.initialized) {
108
+ this.reinitializePlayer();
109
+ }
110
+ break;
111
+ case "lazy":
112
+ __classPrivateFieldSet(this, _YouTubeEmbed_lazy, newValue !== null, "f");
113
+ break;
114
+ case "muted":
115
+ __classPrivateFieldSet(this, _YouTubeEmbed_muted, newValue !== null, "f");
116
+ if (this.player && this.playerReady) {
117
+ if (__classPrivateFieldGet(this, _YouTubeEmbed_muted, "f")) {
118
+ this.player.mute();
119
+ }
120
+ else {
121
+ this.player.unMute();
122
+ }
123
+ }
124
+ break;
125
+ case "poster":
126
+ __classPrivateFieldSet(this, _YouTubeEmbed_poster, newValue || "", "f");
127
+ if (__classPrivateFieldGet(this, _YouTubeEmbed_lazy, "f") && !this.player) {
128
+ this.showPoster(__classPrivateFieldGet(this, _YouTubeEmbed_videoId, "f"));
129
+ }
130
+ break;
131
+ case "background":
132
+ __classPrivateFieldSet(this, _YouTubeEmbed_background, newValue !== null, "f");
133
+ __classPrivateFieldGet(this, _YouTubeEmbed_instances, "m", _YouTubeEmbed_updateBackgroundMode).call(this);
134
+ break;
135
+ case "player-vars":
136
+ try {
137
+ __classPrivateFieldSet(this, _YouTubeEmbed_playerVars, newValue ? JSON.parse(newValue) : {}, "f");
138
+ }
139
+ catch (e) {
140
+ this.error("YouTubeEmbed: Invalid player-vars JSON:", e);
141
+ __classPrivateFieldSet(this, _YouTubeEmbed_playerVars, {}, "f");
142
+ }
143
+ if (this.initialized) {
144
+ this.reinitializePlayer();
145
+ }
146
+ break;
147
+ case "quality":
148
+ if (newValue && this.player && this.playerReady) {
149
+ this.setQuality(newValue);
150
+ }
151
+ break;
152
+ }
153
+ }
154
+ // Getters and setters for attributes
155
+ /**
156
+ * Gets or sets the full YouTube URL.
157
+ * When set, it automatically extracts the video ID and reinitializes the player.
158
+ */
159
+ get url() {
160
+ return __classPrivateFieldGet(this, _YouTubeEmbed_url, "f");
161
+ }
162
+ set url(value) {
163
+ __classPrivateFieldSet(this, _YouTubeEmbed_url, value, "f");
164
+ __classPrivateFieldSet(this, _YouTubeEmbed_videoId, this.extractVideoId(value), "f");
165
+ if (this.initialized) {
166
+ this.reinitializePlayer();
167
+ }
168
+ }
169
+ /**
170
+ * Gets or sets the 11-character YouTube video ID.
171
+ * Reinitializes the player when set.
172
+ */
173
+ get videoId() {
174
+ return __classPrivateFieldGet(this, _YouTubeEmbed_videoId, "f");
175
+ }
176
+ set videoId(value) {
177
+ __classPrivateFieldSet(this, _YouTubeEmbed_videoId, value, "f");
178
+ if (this.initialized) {
179
+ this.reinitializePlayer();
180
+ }
181
+ }
182
+ /**
183
+ * Gets or sets the autoplay state.
184
+ * Note: Autoplay is subject to browser policies and usually requires the video to be muted.
185
+ */
186
+ get autoplay() {
187
+ return __classPrivateFieldGet(this, _YouTubeEmbed_autoplay, "f");
188
+ }
189
+ set autoplay(value) {
190
+ __classPrivateFieldSet(this, _YouTubeEmbed_autoplay, value, "f");
191
+ __classPrivateFieldGet(this, _YouTubeEmbed_instances, "m", _YouTubeEmbed_reflectBooleanAttribute).call(this, "autoplay", value);
192
+ if (!__classPrivateFieldGet(this, _YouTubeEmbed_playing, "f") && value && !__classPrivateFieldGet(this, _YouTubeEmbed_lazy, "f") && this.initialized) {
193
+ this.play();
194
+ }
195
+ }
196
+ /**
197
+ * Gets or sets whether native YouTube player controls are visible.
198
+ */
199
+ get controls() {
200
+ return __classPrivateFieldGet(this, _YouTubeEmbed_controls, "f");
201
+ }
202
+ set controls(value) {
203
+ __classPrivateFieldSet(this, _YouTubeEmbed_controls, value, "f");
204
+ __classPrivateFieldGet(this, _YouTubeEmbed_instances, "m", _YouTubeEmbed_reflectBooleanAttribute).call(this, "controls", value);
205
+ if (this.initialized) {
206
+ this.reinitializePlayer();
207
+ }
208
+ }
209
+ /**
210
+ * Gets or sets the lazy-loading behavior.
211
+ * If true, a poster is shown, and the video loads only on user interaction.
212
+ */
213
+ get lazy() {
214
+ return __classPrivateFieldGet(this, _YouTubeEmbed_lazy, "f");
215
+ }
216
+ set lazy(value) {
217
+ __classPrivateFieldSet(this, _YouTubeEmbed_lazy, value, "f");
218
+ __classPrivateFieldGet(this, _YouTubeEmbed_instances, "m", _YouTubeEmbed_reflectBooleanAttribute).call(this, "lazy", value);
219
+ }
220
+ /**
221
+ * Gets or sets the muted state of the video.
222
+ */
223
+ get muted() {
224
+ return __classPrivateFieldGet(this, _YouTubeEmbed_muted, "f");
225
+ }
226
+ set muted(value) {
227
+ __classPrivateFieldSet(this, _YouTubeEmbed_muted, value, "f");
228
+ __classPrivateFieldGet(this, _YouTubeEmbed_instances, "m", _YouTubeEmbed_reflectBooleanAttribute).call(this, "muted", value);
229
+ if (this.player && this.playerReady) {
230
+ this.updatingMutedState = true;
231
+ if (value) {
232
+ this.player.mute();
233
+ }
234
+ else {
235
+ this.player.unMute();
236
+ }
237
+ // Reset flag after a short delay to allow YouTube API to process
238
+ setTimeout(() => {
239
+ this.updatingMutedState = false;
240
+ }, 100);
241
+ }
242
+ }
243
+ /**
244
+ * Gets or sets the URL for a custom poster image.
245
+ * If not set, a default YouTube thumbnail is used for lazy-loaded videos.
246
+ */
247
+ get poster() {
248
+ return __classPrivateFieldGet(this, _YouTubeEmbed_poster, "f");
249
+ }
250
+ set poster(value) {
251
+ __classPrivateFieldSet(this, _YouTubeEmbed_poster, value, "f");
252
+ this.updatingAttribute = true;
253
+ if (value) {
254
+ this.setAttribute("poster", value);
255
+ }
256
+ else {
257
+ this.removeAttribute("poster");
258
+ }
259
+ this.updatingAttribute = false;
260
+ if (__classPrivateFieldGet(this, _YouTubeEmbed_lazy, "f") && !this.player) {
261
+ this.showPoster(__classPrivateFieldGet(this, _YouTubeEmbed_videoId, "f"));
262
+ }
263
+ }
264
+ /**
265
+ * Returns `true` if the video is currently playing.
266
+ */
267
+ get playing() {
268
+ return __classPrivateFieldGet(this, _YouTubeEmbed_playing, "f");
269
+ }
270
+ /**
271
+ * Gets or sets the background mode.
272
+ * In background mode, the video covers its container, is muted, and has no controls.
273
+ */
274
+ get background() {
275
+ return __classPrivateFieldGet(this, _YouTubeEmbed_background, "f");
276
+ }
277
+ set background(value) {
278
+ __classPrivateFieldSet(this, _YouTubeEmbed_background, value, "f");
279
+ __classPrivateFieldGet(this, _YouTubeEmbed_instances, "m", _YouTubeEmbed_reflectBooleanAttribute).call(this, "background", value);
280
+ __classPrivateFieldGet(this, _YouTubeEmbed_instances, "m", _YouTubeEmbed_updateBackgroundMode).call(this);
281
+ }
282
+ /**
283
+ * Gets or sets extra player parameters.
284
+ * @param {Record<string, string | number | boolean>} vars - An object of key-value pairs to pass to the player.
285
+ */
286
+ get playerVars() {
287
+ return __classPrivateFieldGet(this, _YouTubeEmbed_playerVars, "f");
288
+ }
289
+ set playerVars(vars) {
290
+ __classPrivateFieldSet(this, _YouTubeEmbed_playerVars, vars, "f");
291
+ this.setAttribute("player-vars", JSON.stringify(vars));
292
+ if (this.initialized) {
293
+ this.reinitializePlayer();
294
+ }
295
+ }
296
+ extractVideoId(url) {
297
+ if (!url)
298
+ return "";
299
+ // Comprehensive regex to handle various YouTube URL formats
300
+ const regex = /(?:youtube\.com\/(?:[^\/]+\/.+\/|(?:v|e(?:mbed)?)\/|.*[?&]v=)|youtu\.be\/)([^"&?\/ ]{11})/;
301
+ const match = url.match(regex);
302
+ if (match && match[1]) {
303
+ return match[1];
304
+ }
305
+ // If it's already just an ID
306
+ if (/^[a-zA-Z0-9_-]{11}$/.test(url)) {
307
+ return url;
308
+ }
309
+ this.warn(`YouTubeEmbed: Could not extract video ID from "${url}"`);
310
+ return "";
311
+ }
312
+ log(...args) {
313
+ if (YouTubeEmbed.DEBUG) {
314
+ console.log("YouTubeEmbed:", ...args);
315
+ }
316
+ }
317
+ warn(...args) {
318
+ console.warn(...args);
319
+ }
320
+ error(...args) {
321
+ console.error(...args);
322
+ }
323
+ dispatchCustomEvent(eventName, detail) {
324
+ this.dispatchEvent(new CustomEvent(eventName, {
325
+ detail,
326
+ bubbles: true,
327
+ composed: true,
328
+ }));
329
+ }
330
+ isValidPosterUrl(url) {
331
+ if (!url)
332
+ return false;
333
+ try {
334
+ new URL(url);
335
+ // Exclude known problematic domains
336
+ return true;
337
+ }
338
+ catch (_a) {
339
+ return false;
340
+ }
341
+ }
342
+ /**
343
+ * Setup fullscreen change event listener
344
+ */
345
+ setupFullscreenListener() {
346
+ const handleFullscreenChange = () => {
347
+ const isFullscreen = this.isFullscreen();
348
+ this.dispatchCustomEvent("fullscreenchange", { isFullscreen });
349
+ if (isFullscreen) {
350
+ this.classList.add("is-fullscreen");
351
+ }
352
+ else {
353
+ this.classList.remove("is-fullscreen");
354
+ }
355
+ };
356
+ document.addEventListener("fullscreenchange", handleFullscreenChange);
357
+ document.addEventListener("webkitfullscreenchange", handleFullscreenChange);
358
+ document.addEventListener("mozfullscreenchange", handleFullscreenChange);
359
+ document.addEventListener("MSFullscreenChange", handleFullscreenChange);
360
+ }
361
+ /**
362
+ * Setup keyboard event handlers for accessibility
363
+ */
364
+ setupKeyboardHandlers() {
365
+ this.keyboardHandler = (e) => {
366
+ var _a, _b;
367
+ // Only handle if player is ready and not in input element
368
+ if (!this.playerReady || ((_a = e.target) === null || _a === void 0 ? void 0 : _a.tagName) === "INPUT") {
369
+ return;
370
+ }
371
+ switch (e.key.toLowerCase()) {
372
+ case "k":
373
+ case " ":
374
+ // Play/Pause
375
+ e.preventDefault();
376
+ this.togglePlay();
377
+ this.announceToScreenReader(__classPrivateFieldGet(this, _YouTubeEmbed_playing, "f") ? "Video playing" : "Video paused");
378
+ break;
379
+ case "m":
380
+ // Mute/Unmute
381
+ e.preventDefault();
382
+ this.toggleMute();
383
+ (_b = this.player) === null || _b === void 0 ? void 0 : _b.isMuted().then((muted) => {
384
+ this.announceToScreenReader(muted ? "Video muted" : "Video unmuted");
385
+ });
386
+ break;
387
+ case "f":
388
+ // Fullscreen toggle
389
+ e.preventDefault();
390
+ if (document.fullscreenElement) {
391
+ document.exitFullscreen();
392
+ this.announceToScreenReader("Exited fullscreen");
393
+ }
394
+ else if (this.enterFullscreen) {
395
+ this.enterFullscreen();
396
+ this.announceToScreenReader("Entered fullscreen");
397
+ }
398
+ break;
399
+ }
400
+ };
401
+ this.addEventListener("keydown", this.keyboardHandler);
402
+ }
403
+ /**
404
+ * Announce message to screen readers
405
+ */
406
+ announceToScreenReader(message) {
407
+ if (this.ariaLiveRegion) {
408
+ this.ariaLiveRegion.textContent = message;
409
+ // Clear after a brief delay
410
+ setTimeout(() => {
411
+ if (this.ariaLiveRegion) {
412
+ this.ariaLiveRegion.textContent = "";
413
+ }
414
+ }, 1000);
415
+ }
416
+ }
417
+ /**
418
+ * Add preconnect and dns-prefetch hints for YouTube domains
419
+ */
420
+ addResourceHints() {
421
+ const hints = [
422
+ { rel: "preconnect", href: "https://www.youtube.com" },
423
+ { rel: "preconnect", href: "https://i.ytimg.com" },
424
+ { rel: "dns-prefetch", href: "https://www.youtube.com" },
425
+ { rel: "dns-prefetch", href: "https://i.ytimg.com" },
426
+ ];
427
+ hints.forEach(({ rel, href }) => {
428
+ if (!document.querySelector(`link[rel="${rel}"][href="${href}"]`)) {
429
+ const link = document.createElement("link");
430
+ link.rel = rel;
431
+ link.href = href;
432
+ if (rel === "preconnect") {
433
+ link.crossOrigin = "anonymous";
434
+ }
435
+ document.head.appendChild(link);
436
+ }
437
+ });
438
+ }
439
+ /**
440
+ * Setup Intersection Observer for lazy loading
441
+ */
442
+ setupIntersectionObserver() {
443
+ if (!("IntersectionObserver" in window)) {
444
+ // Fallback: load immediately if IntersectionObserver not supported
445
+ return;
446
+ }
447
+ const options = {
448
+ root: null,
449
+ rootMargin: "50px", // Start loading 50px before entering viewport
450
+ threshold: 0.01,
451
+ };
452
+ this.intersectionObserver = new IntersectionObserver((entries) => {
453
+ entries.forEach((entry) => {
454
+ if (entry.isIntersecting && !this.hasLoadedVideo) {
455
+ this.hasLoadedVideo = true;
456
+ this.log("YouTubeEmbed: Video entering viewport, loading...");
457
+ // Check if poster was clicked before intersection
458
+ const shouldAutoplay = this.getAttribute("data-poster-autoplay") === "true";
459
+ if (!shouldAutoplay) {
460
+ // Just load the player, don't autoplay
461
+ this.initializePlayer(__classPrivateFieldGet(this, _YouTubeEmbed_videoId, "f"));
462
+ }
463
+ // Disconnect observer after loading
464
+ if (this.intersectionObserver) {
465
+ this.intersectionObserver.disconnect();
466
+ }
467
+ }
468
+ });
469
+ }, options);
470
+ this.intersectionObserver.observe(this);
471
+ }
472
+ /**
473
+ * Display error message to user with retry option
474
+ */
475
+ showErrorMessage(message) {
476
+ const errorDiv = document.createElement("div");
477
+ errorDiv.className = "youtube-error-message";
478
+ errorDiv.setAttribute("role", "alert");
479
+ errorDiv.innerHTML = `
480
+ <div class="error-content">
481
+ <svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
482
+ <circle cx="12" cy="12" r="10"></circle>
483
+ <line x1="12" y1="8" x2="12" y2="12"></line>
484
+ <line x1="12" y1="16" x2="12.01" y2="16"></line>
485
+ </svg>
486
+ <p class="error-message">${message}</p>
487
+ <button class="retry-button">Retry</button>
488
+ </div>
489
+ `;
490
+ const retryButton = errorDiv.querySelector(".retry-button");
491
+ if (retryButton) {
492
+ retryButton.addEventListener("click", () => {
493
+ this.apiLoadRetries = 0;
494
+ errorDiv.remove();
495
+ if (__classPrivateFieldGet(this, _YouTubeEmbed_videoId, "f")) {
496
+ this.initializePlayer(__classPrivateFieldGet(this, _YouTubeEmbed_videoId, "f"));
497
+ }
498
+ });
499
+ }
500
+ // Clear existing content
501
+ this.innerHTML = "";
502
+ this.appendChild(errorDiv);
503
+ }
504
+ connectedCallback() {
505
+ YouTubeEmbed.instanceCount++;
506
+ this.log("YouTubeEmbed: connectedCallback called, instance count:", YouTubeEmbed.instanceCount);
507
+ // Add resource hints on first instance
508
+ if (YouTubeEmbed.instanceCount === 1) {
509
+ this.addResourceHints();
510
+ }
511
+ // Initialize attributes from HTML
512
+ const urlAttr = this.getAttribute("url");
513
+ const videoIdAttr = this.getAttribute("video-id");
514
+ if (urlAttr) {
515
+ __classPrivateFieldSet(this, _YouTubeEmbed_url, urlAttr, "f");
516
+ __classPrivateFieldSet(this, _YouTubeEmbed_videoId, this.extractVideoId(urlAttr), "f");
517
+ }
518
+ else if (videoIdAttr) {
519
+ __classPrivateFieldSet(this, _YouTubeEmbed_videoId, videoIdAttr, "f");
520
+ }
521
+ // Set boolean attributes
522
+ __classPrivateFieldSet(this, _YouTubeEmbed_autoplay, this.hasAttribute("autoplay"), "f");
523
+ __classPrivateFieldSet(this, _YouTubeEmbed_controls, this.hasAttribute("controls"), "f");
524
+ __classPrivateFieldSet(this, _YouTubeEmbed_lazy, this.hasAttribute("lazy"), "f");
525
+ __classPrivateFieldSet(this, _YouTubeEmbed_muted, this.hasAttribute("muted"), "f");
526
+ __classPrivateFieldSet(this, _YouTubeEmbed_background, this.hasAttribute("background"), "f");
527
+ // Set string attributes
528
+ __classPrivateFieldSet(this, _YouTubeEmbed_poster, this.getAttribute("poster") || "", "f");
529
+ try {
530
+ const playerVarsAttr = this.getAttribute("player-vars");
531
+ __classPrivateFieldSet(this, _YouTubeEmbed_playerVars, playerVarsAttr ? JSON.parse(playerVarsAttr) : {}, "f");
532
+ }
533
+ catch (e) {
534
+ this.error("YouTubeEmbed: Invalid player-vars JSON on init:", e);
535
+ __classPrivateFieldSet(this, _YouTubeEmbed_playerVars, {}, "f");
536
+ }
537
+ if (!__classPrivateFieldGet(this, _YouTubeEmbed_videoId, "f")) {
538
+ this.error("YouTubeEmbed: Missing or invalid video ID");
539
+ this.dispatchCustomEvent("error", {
540
+ message: "Missing or invalid video ID",
541
+ });
542
+ return;
543
+ }
544
+ // Use the element itself as the container
545
+ this.classList.add("youtube-embed-wrapper");
546
+ // Add ARIA role and tabindex for accessibility
547
+ this.setAttribute("role", "region");
548
+ this.setAttribute("aria-label", "Video player");
549
+ if (!this.hasAttribute("tabindex")) {
550
+ this.setAttribute("tabindex", "0");
551
+ }
552
+ // Create ARIA live region for screen reader announcements
553
+ this.ariaLiveRegion = document.createElement("div");
554
+ this.ariaLiveRegion.setAttribute("aria-live", "polite");
555
+ this.ariaLiveRegion.setAttribute("aria-atomic", "true");
556
+ this.ariaLiveRegion.className = "sr-only";
557
+ this.ariaLiveRegion.style.cssText =
558
+ "position:absolute;left:-10000px;width:1px;height:1px;overflow:hidden;";
559
+ this.appendChild(this.ariaLiveRegion);
560
+ // Setup keyboard event handlers
561
+ this.setupKeyboardHandlers();
562
+ // Setup fullscreen change listener
563
+ this.setupFullscreenListener();
564
+ __classPrivateFieldGet(this, _YouTubeEmbed_instances, "m", _YouTubeEmbed_updateBackgroundMode).call(this);
565
+ if (__classPrivateFieldGet(this, _YouTubeEmbed_lazy, "f")) {
566
+ this.showPoster(__classPrivateFieldGet(this, _YouTubeEmbed_videoId, "f"));
567
+ // Setup Intersection Observer for better performance
568
+ this.setupIntersectionObserver();
569
+ }
570
+ else {
571
+ // Initialize the player immediately
572
+ this.initializePlayer(__classPrivateFieldGet(this, _YouTubeEmbed_videoId, "f"));
573
+ }
574
+ this.initialized = true;
575
+ this.dispatchCustomEvent("connected");
576
+ }
577
+ disconnectedCallback() {
578
+ YouTubeEmbed.instanceCount--;
579
+ this.log("YouTubeEmbed: disconnectedCallback called, remaining instances:", YouTubeEmbed.instanceCount);
580
+ // Stop the video and destroy the player instance
581
+ if (this.player) {
582
+ try {
583
+ this.player.destroy();
584
+ this.log("YouTubeEmbed: Player destroyed.");
585
+ }
586
+ catch (error) {
587
+ this.warn("YouTubeEmbed: Error destroying player:", error);
588
+ }
589
+ this.player = null;
590
+ }
591
+ // Remove keyboard event handlers
592
+ if (this.keyboardHandler) {
593
+ this.removeEventListener("keydown", this.keyboardHandler);
594
+ this.keyboardHandler = null;
595
+ }
596
+ // Remove event listeners from poster and play button
597
+ const poster = this.querySelector(".youtube-poster");
598
+ const buttonOverlay = this.querySelector(".button-overlay");
599
+ if (poster && this.posterClickHandler) {
600
+ poster.removeEventListener("click", this.posterClickHandler);
601
+ this.posterClickHandler = null;
602
+ }
603
+ if (buttonOverlay && this.posterClickHandler) {
604
+ buttonOverlay.removeEventListener("click", this.posterClickHandler);
605
+ }
606
+ // Clear element content
607
+ this.innerHTML = "";
608
+ this.classList.remove("youtube-embed-wrapper", "youtube-embed-container", "is-playing");
609
+ // Only clean up global resources if this is the last instance
610
+ if (YouTubeEmbed.instanceCount === 0) {
611
+ this.log("YouTubeEmbed: Last instance, cleaning up global resources");
612
+ // Remove the YouTube API script
613
+ const script = document.querySelector('script[src="https://www.youtube.com/iframe_api"]');
614
+ if (script) {
615
+ script.remove();
616
+ this.log("YouTubeEmbed: YouTube API script removed.");
617
+ }
618
+ // Clear global references
619
+ if (window.onYouTubeIframeAPIReady) {
620
+ delete window.onYouTubeIframeAPIReady;
621
+ }
622
+ // Reset static flags
623
+ YouTubeEmbed.apiLoaded = false;
624
+ YouTubeEmbed.apiReady = false;
625
+ }
626
+ // Nullify DOM references
627
+ this.iframe = null;
628
+ this.playPauseButton = null;
629
+ this.setCustomControlState = null;
630
+ // Dispatch disconnected event
631
+ this.dispatchCustomEvent("disconnected");
632
+ this.log("YouTubeEmbed: Cleanup complete.");
633
+ }
634
+ reinitializePlayer() {
635
+ if (!this.initialized || !__classPrivateFieldGet(this, _YouTubeEmbed_videoId, "f")) {
636
+ return;
637
+ }
638
+ // Remove existing player
639
+ if (this.player) {
640
+ try {
641
+ this.player.destroy();
642
+ }
643
+ catch (error) {
644
+ this.warn("YouTubeEmbed: Error destroying player:", error);
645
+ }
646
+ }
647
+ // Clear and reinitialize
648
+ if (__classPrivateFieldGet(this, _YouTubeEmbed_lazy, "f")) {
649
+ this.showPoster(__classPrivateFieldGet(this, _YouTubeEmbed_videoId, "f"));
650
+ }
651
+ else {
652
+ this.initializePlayer(__classPrivateFieldGet(this, _YouTubeEmbed_videoId, "f"));
653
+ }
654
+ }
655
+ showPoster(videoId) {
656
+ // Use custom poster if valid, otherwise use YouTube thumbnail
657
+ const thumbnailUrl = this.isValidPosterUrl(__classPrivateFieldGet(this, _YouTubeEmbed_poster, "f"))
658
+ ? __classPrivateFieldGet(this, _YouTubeEmbed_poster, "f")
659
+ : `https://img.youtube.com/vi/${videoId}/hqdefault.jpg`;
660
+ this.log("YouTubeEmbed: Using poster URL:", thumbnailUrl);
661
+ // Clear any existing content and event listeners
662
+ this.innerHTML = "";
663
+ // Clear old event listeners if they exist
664
+ if (this.posterClickHandler) {
665
+ this.posterClickHandler = null;
666
+ }
667
+ // Create poster image
668
+ const poster = document.createElement("img");
669
+ poster.src = thumbnailUrl;
670
+ poster.alt = "YouTube Video Thumbnail";
671
+ poster.classList.add("youtube-poster", "video-poster"); // Add both classes for compatibility
672
+ poster.loading = "lazy"; // Optimize image loading
673
+ // Add error handling for poster loading
674
+ poster.onerror = () => {
675
+ this.warn("YouTubeEmbed: Poster failed to load, using white background.");
676
+ poster.style.display = "none";
677
+ this.style.backgroundColor = "#FFFFFF";
678
+ };
679
+ // Create button overlay with play button
680
+ const buttonOverlay = document.createElement("div");
681
+ buttonOverlay.classList.add("button-overlay");
682
+ buttonOverlay.setAttribute("role", "button");
683
+ buttonOverlay.setAttribute("tabindex", "0");
684
+ buttonOverlay.setAttribute("aria-label", "Play video");
685
+ const button = document.createElement("div");
686
+ button.classList.add("button");
687
+ button.setAttribute("aria-hidden", "true");
688
+ buttonOverlay.appendChild(button);
689
+ // Handle click to load video - store reference for cleanup
690
+ const loadVideo = () => {
691
+ this.log("YouTubeEmbed: Loading video from poster click");
692
+ this.setAttribute("data-poster-autoplay", "true");
693
+ try {
694
+ this.initializePlayer(videoId);
695
+ }
696
+ catch (error) {
697
+ this.error("YouTubeEmbed: Error initializing player from poster:", error);
698
+ this.dispatchCustomEvent("error", { message: "Failed to load video" });
699
+ }
700
+ };
701
+ // Store reference for cleanup
702
+ this.posterClickHandler = loadVideo;
703
+ // Add keyboard support
704
+ const keyboardActivate = (e) => {
705
+ if (e.key === "Enter" || e.key === " ") {
706
+ e.preventDefault();
707
+ loadVideo(e);
708
+ }
709
+ };
710
+ poster.addEventListener("click", loadVideo);
711
+ buttonOverlay.addEventListener("click", loadVideo);
712
+ buttonOverlay.addEventListener("keydown", keyboardActivate);
713
+ this.appendChild(poster);
714
+ this.appendChild(buttonOverlay);
715
+ this.log("YouTubeEmbed: Poster displayed for lazy loading");
716
+ }
717
+ initializePlayer(videoId) {
718
+ return __awaiter(this, void 0, void 0, function* () {
719
+ this.log("YouTubeEmbed: Initializing player for video ID:", videoId);
720
+ const isBackground = __classPrivateFieldGet(this, _YouTubeEmbed_background, "f");
721
+ // Background videos must autoplay and be muted, with no controls.
722
+ const autoplay = isBackground ||
723
+ this.hasAttribute("autoplay") ||
724
+ this.hasAttribute("data-poster-autoplay") ||
725
+ this.hasAttribute("data-should-autoplay");
726
+ const controls = isBackground ? false : this.hasAttribute("controls");
727
+ const mute = isBackground || autoplay; // Mute if background or if autoplaying
728
+ // Remove the temporary autoplay flags
729
+ if (this.hasAttribute("data-poster-autoplay")) {
730
+ this.removeAttribute("data-poster-autoplay");
731
+ }
732
+ if (this.hasAttribute("data-should-autoplay")) {
733
+ this.removeAttribute("data-should-autoplay");
734
+ }
735
+ this.log("YouTubeEmbed: Creating iframe with autoplay:", autoplay, "controls:", controls);
736
+ this.iframe = document.createElement("iframe");
737
+ // Build URL parameters to handle YouTube's strict policies
738
+ const params = new URLSearchParams(Object.assign({ enablejsapi: "1", autoplay: autoplay ? "1" : "0", controls: !controls && !isBackground ? "1" : "0", mute: mute ? "1" : "0", playsinline: "1", rel: "0", modestbranding: "1", origin: window.location.origin || "localhost" }, __classPrivateFieldGet(this, _YouTubeEmbed_playerVars, "f")));
739
+ // Add muted state if explicitly set
740
+ if (__classPrivateFieldGet(this, _YouTubeEmbed_muted, "f")) {
741
+ params.set("mute", "1");
742
+ }
743
+ // Update internal muted state if autoplay forces muting
744
+ if (autoplay && !__classPrivateFieldGet(this, _YouTubeEmbed_muted, "f")) {
745
+ __classPrivateFieldSet(this, _YouTubeEmbed_muted, true, "f");
746
+ this.log("YouTubeEmbed: Autoplay enabled, forcing muted state for compliance");
747
+ }
748
+ this.iframe.src = `https://www.youtube.com/embed/${videoId}?${params.toString()}`;
749
+ this.iframe.allow =
750
+ "accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share";
751
+ this.iframe.allowFullscreen = true;
752
+ this.iframe.style.position = "absolute";
753
+ this.iframe.style.border = "none";
754
+ // Disable pointer events in background mode to allow clicks to pass through
755
+ if (__classPrivateFieldGet(this, _YouTubeEmbed_background, "f")) {
756
+ this.iframe.style.pointerEvents = "none";
757
+ // CSS handles the sizing for background videos
758
+ }
759
+ else {
760
+ this.iframe.style.top = "0";
761
+ this.iframe.style.left = "0";
762
+ this.iframe.style.width = "100%";
763
+ this.iframe.style.height = "100%";
764
+ }
765
+ this.log("YouTubeEmbed: Iframe created with src:", this.iframe.src);
766
+ this.replaceChildren(this.iframe);
767
+ if (!controls) {
768
+ this.addCustomControls();
769
+ }
770
+ // Initialize the YouTube API player for advanced control
771
+ try {
772
+ yield YouTubeEmbed.loadYouTubeAPIWithRetry(this.apiLoadRetries);
773
+ this.player = new YT.Player(this.iframe, {
774
+ videoId,
775
+ events: {
776
+ onReady: () => {
777
+ this.log("YouTubeEmbed: Player is ready.");
778
+ this.playerReady = true;
779
+ // Sync muted state with YouTube player
780
+ if (__classPrivateFieldGet(this, _YouTubeEmbed_muted, "f")) {
781
+ this.player.mute();
782
+ }
783
+ else {
784
+ this.player.unMute();
785
+ }
786
+ // Check if autoplay was enabled during initialization and update custom control state
787
+ if (autoplay && this.setCustomControlState) {
788
+ this.setCustomControlState(true);
789
+ __classPrivateFieldSet(this, _YouTubeEmbed_playing, true, "f");
790
+ }
791
+ this.dispatchCustomEvent("ready");
792
+ },
793
+ onStateChange: (event) => {
794
+ const isPlaying = event.data === YT.PlayerState.PLAYING;
795
+ const isPaused = event.data === YT.PlayerState.PAUSED;
796
+ const isEnded = event.data === YT.PlayerState.ENDED;
797
+ __classPrivateFieldSet(this, _YouTubeEmbed_playing, isPlaying, "f");
798
+ // Sync muted state with actual player state (only if not programmatically updating)
799
+ try {
800
+ if (!this.updatingMutedState &&
801
+ this.player &&
802
+ typeof this.player.isMuted === "function") {
803
+ const actualMuted = this.player.isMuted();
804
+ if (actualMuted !== __classPrivateFieldGet(this, _YouTubeEmbed_muted, "f")) {
805
+ __classPrivateFieldSet(this, _YouTubeEmbed_muted, actualMuted, "f");
806
+ this.log("YouTubeEmbed: Synced muted state:", actualMuted);
807
+ }
808
+ }
809
+ }
810
+ catch (error) {
811
+ this.warn("YouTubeEmbed: Could not sync muted state:", error);
812
+ }
813
+ // Update custom control state based on player state
814
+ if (this.setCustomControlState) {
815
+ this.setCustomControlState(isPlaying);
816
+ }
817
+ // Dispatch appropriate events
818
+ if (isPlaying) {
819
+ this.dispatchCustomEvent("play");
820
+ }
821
+ else if (isPaused) {
822
+ this.dispatchCustomEvent("pause");
823
+ }
824
+ else if (isEnded) {
825
+ this.dispatchCustomEvent("ended");
826
+ }
827
+ },
828
+ onError: (event) => {
829
+ this.error("YouTubeEmbed: Player encountered an error:", event);
830
+ let errorMessage = "Unknown error occurred";
831
+ // Handle specific YouTube error codes
832
+ switch (event.data) {
833
+ case 2:
834
+ errorMessage = "Invalid video ID";
835
+ break;
836
+ case 5:
837
+ errorMessage = "HTML5 player error";
838
+ break;
839
+ case 100:
840
+ errorMessage = "Video not found or private";
841
+ break;
842
+ case 101:
843
+ case 150:
844
+ errorMessage =
845
+ "Video cannot be embedded (restrictions by owner)";
846
+ break;
847
+ default:
848
+ errorMessage = `YouTube error code: ${event.data}`;
849
+ }
850
+ this.dispatchCustomEvent("error", {
851
+ message: errorMessage,
852
+ code: event.data,
853
+ });
854
+ },
855
+ },
856
+ });
857
+ }
858
+ catch (error) {
859
+ this.error("YouTubeEmbed: YouTube API initialization failed:", error);
860
+ this.showErrorMessage(error instanceof Error
861
+ ? error.message
862
+ : "Failed to load YouTube player. Please refresh the page or check your connection.");
863
+ this.dispatchCustomEvent("error", {
864
+ message: error instanceof Error ? error.message : "API load failed",
865
+ code: "API_LOAD_ERROR",
866
+ retryable: true,
867
+ });
868
+ }
869
+ });
870
+ }
871
+ static loadYouTubeAPI() {
872
+ return __awaiter(this, void 0, void 0, function* () {
873
+ return new Promise((resolve, reject) => {
874
+ if (this.apiReady) {
875
+ resolve();
876
+ return;
877
+ }
878
+ const timeoutId = setTimeout(() => {
879
+ cleanup();
880
+ reject(new Error("YouTube API loading timeout. The API script may be blocked by an ad blocker or network issue."));
881
+ }, this.API_LOAD_TIMEOUT);
882
+ const cleanup = () => {
883
+ clearTimeout(timeoutId);
884
+ delete window.onYouTubeIframeAPIReady;
885
+ };
886
+ if (!this.apiLoaded) {
887
+ const script = document.createElement("script");
888
+ script.src = "https://www.youtube.com/iframe_api";
889
+ script.onerror = () => {
890
+ cleanup();
891
+ reject(new Error("Failed to load YouTube API script. Please check your network connection or disable ad blockers."));
892
+ };
893
+ document.head.appendChild(script);
894
+ this.apiLoaded = true;
895
+ }
896
+ window.onYouTubeIframeAPIReady = () => {
897
+ this.apiReady = true;
898
+ cleanup();
899
+ resolve();
900
+ };
901
+ });
902
+ });
903
+ }
904
+ /**
905
+ * Load YouTube API with retry mechanism
906
+ */
907
+ static loadYouTubeAPIWithRetry() {
908
+ return __awaiter(this, arguments, void 0, function* (retries = 0) {
909
+ try {
910
+ yield this.loadYouTubeAPI();
911
+ }
912
+ catch (error) {
913
+ if (retries < this.MAX_API_RETRIES) {
914
+ console.warn(`YouTubeEmbed: API load failed, retrying (${retries + 1}/${this.MAX_API_RETRIES})...`);
915
+ yield new Promise((resolve) => setTimeout(resolve, this.API_RETRY_DELAY));
916
+ return this.loadYouTubeAPIWithRetry(retries + 1);
917
+ }
918
+ throw error;
919
+ }
920
+ });
921
+ }
922
+ addCustomControls() {
923
+ this.classList.add("youtube-embed-container");
924
+ // Create button overlay
925
+ const buttonOverlay = document.createElement("div");
926
+ buttonOverlay.classList.add("button-overlay");
927
+ this.playPauseButton = document.createElement("div");
928
+ this.playPauseButton.classList.add("button");
929
+ let isPlaying = false;
930
+ const setPlayingState = (playing) => {
931
+ isPlaying = playing;
932
+ if (playing) {
933
+ this.classList.add("is-playing");
934
+ }
935
+ else {
936
+ this.classList.remove("is-playing");
937
+ }
938
+ };
939
+ const togglePlayPause = () => {
940
+ if (isPlaying) {
941
+ // Pause the video
942
+ if (this.player && typeof this.player.pauseVideo === "function") {
943
+ this.player.pauseVideo();
944
+ }
945
+ else if (this.iframe && this.iframe.contentWindow) {
946
+ this.iframe.contentWindow.postMessage('{"event":"command","func":"pauseVideo","args":""}', "*");
947
+ }
948
+ setPlayingState(false);
949
+ }
950
+ else {
951
+ // Play the video
952
+ if (this.player && typeof this.player.playVideo === "function") {
953
+ this.player.playVideo();
954
+ }
955
+ else if (this.iframe && this.iframe.contentWindow) {
956
+ this.iframe.contentWindow.postMessage('{"event":"command","func":"playVideo","args":""}', "*");
957
+ }
958
+ setPlayingState(true);
959
+ }
960
+ };
961
+ // Store reference to setPlayingState for external control
962
+ this.setCustomControlState = setPlayingState;
963
+ buttonOverlay.addEventListener("click", togglePlayPause);
964
+ if (this.playPauseButton) {
965
+ buttonOverlay.appendChild(this.playPauseButton);
966
+ }
967
+ this.appendChild(buttonOverlay);
968
+ }
969
+ // Update the playVideo method to wait for the player to be ready
970
+ /**
971
+ * Plays the video. If the video is lazy-loaded and not yet initialized, it will be loaded first.
972
+ */
973
+ play() {
974
+ return __awaiter(this, void 0, void 0, function* () {
975
+ // Check if this is a lazy-loaded video that hasn't been initialized yet
976
+ if (__classPrivateFieldGet(this, _YouTubeEmbed_lazy, "f") && !this.player && !this.iframe) {
977
+ this.log("YouTubeEmbed: Lazy video needs to be loaded first. Initializing...");
978
+ // Set a flag to auto-play after initialization
979
+ this.setAttribute("data-should-autoplay", "true");
980
+ // Initialize the player (this will load the video and replace the poster)
981
+ yield this.initializePlayer(__classPrivateFieldGet(this, _YouTubeEmbed_videoId, "f"));
982
+ // Wait a bit for the player to be ready after initialization
983
+ yield new Promise((resolve) => setTimeout(resolve, 1000));
984
+ }
985
+ if (!this.playerReady) {
986
+ this.warn("YouTubeEmbed: Player is not ready. Waiting for initialization.");
987
+ yield new Promise((resolve) => {
988
+ const interval = setInterval(() => {
989
+ if (this.playerReady) {
990
+ clearInterval(interval);
991
+ resolve(null);
992
+ }
993
+ }, 100);
994
+ // Timeout after 5 seconds
995
+ setTimeout(() => {
996
+ clearInterval(interval);
997
+ resolve(null);
998
+ }, 5000);
999
+ });
1000
+ }
1001
+ // Remove the autoplay flag if it was set
1002
+ this.removeAttribute("data-should-autoplay");
1003
+ if (this.player && typeof this.player.playVideo === "function") {
1004
+ this.player.playVideo();
1005
+ this.log("YouTubeEmbed: Video playback started via API.");
1006
+ }
1007
+ else {
1008
+ this.log("YouTubeEmbed: Using iframe postMessage for play.");
1009
+ // Fallback: try to use postMessage to control iframe
1010
+ if (this.iframe && this.iframe.contentWindow) {
1011
+ this.iframe.contentWindow.postMessage('{"event":"command","func":"playVideo","args":""}', "*");
1012
+ }
1013
+ }
1014
+ __classPrivateFieldSet(this, _YouTubeEmbed_playing, true, "f");
1015
+ // Update custom control state
1016
+ if (this.setCustomControlState) {
1017
+ this.setCustomControlState(true);
1018
+ }
1019
+ this.dispatchCustomEvent("play");
1020
+ });
1021
+ }
1022
+ /**
1023
+ * Pauses the currently playing video.
1024
+ */
1025
+ pause() {
1026
+ if (this.player && typeof this.player.pauseVideo === "function") {
1027
+ this.player.pauseVideo();
1028
+ this.log("YouTubeEmbed: Video paused via API.");
1029
+ }
1030
+ else {
1031
+ this.log("YouTubeEmbed: Using iframe postMessage for pause.");
1032
+ // Fallback: try to use postMessage to control iframe
1033
+ if (this.iframe && this.iframe.contentWindow) {
1034
+ this.iframe.contentWindow.postMessage('{"event":"command","func":"pauseVideo","args":""}', "*");
1035
+ }
1036
+ }
1037
+ __classPrivateFieldSet(this, _YouTubeEmbed_playing, false, "f");
1038
+ // Update custom control state
1039
+ if (this.setCustomControlState) {
1040
+ this.setCustomControlState(false);
1041
+ }
1042
+ this.dispatchCustomEvent("pause");
1043
+ }
1044
+ /**
1045
+ * Stops the video and resets it to the beginning.
1046
+ */
1047
+ stopVideo() {
1048
+ if (this.player && typeof this.player.stopVideo === "function") {
1049
+ this.player.stopVideo();
1050
+ this.log("YouTubeEmbed: Video stopped via API.");
1051
+ }
1052
+ else {
1053
+ this.log("YouTubeEmbed: Using iframe postMessage for stop.");
1054
+ // Fallback: try to use postMessage to control iframe
1055
+ if (this.iframe && this.iframe.contentWindow) {
1056
+ this.iframe.contentWindow.postMessage('{"event":"command","func":"stopVideo","args":""}', "*");
1057
+ }
1058
+ }
1059
+ __classPrivateFieldSet(this, _YouTubeEmbed_playing, false, "f");
1060
+ // Update custom control state
1061
+ if (this.setCustomControlState) {
1062
+ this.setCustomControlState(false);
1063
+ }
1064
+ this.dispatchCustomEvent("stop");
1065
+ }
1066
+ /**
1067
+ * Mutes the video audio.
1068
+ */
1069
+ mute() {
1070
+ this.muted = true;
1071
+ }
1072
+ /**
1073
+ * Unmutes the video audio.
1074
+ */
1075
+ unmute() {
1076
+ this.muted = false;
1077
+ }
1078
+ /**
1079
+ * Toggles the video between playing and paused.
1080
+ */
1081
+ togglePlay() {
1082
+ if (__classPrivateFieldGet(this, _YouTubeEmbed_playing, "f")) {
1083
+ this.pause();
1084
+ }
1085
+ else {
1086
+ this.play();
1087
+ }
1088
+ }
1089
+ /**
1090
+ * Toggles the audio between muted and unmuted.
1091
+ */
1092
+ toggleMute() {
1093
+ this.muted = !__classPrivateFieldGet(this, _YouTubeEmbed_muted, "f");
1094
+ }
1095
+ /**
1096
+ * Toggles the debug logging for all component instances.
1097
+ * @param {boolean} [forceState] - Optional: force debug mode on or off.
1098
+ */
1099
+ static toggleDebug(forceState) {
1100
+ YouTubeEmbed.DEBUG =
1101
+ forceState !== undefined ? forceState : !YouTubeEmbed.DEBUG;
1102
+ console.log(`YouTubeEmbed: Debugging is now ${YouTubeEmbed.DEBUG ? "ENABLED" : "DISABLED"}.`);
1103
+ }
1104
+ // Public API for getting player state
1105
+ /**
1106
+ * Retrieves the current state of the player.
1107
+ * @returns An object with the current player state.
1108
+ */
1109
+ getPlayerState() {
1110
+ // Get actual muted state from YouTube player if available
1111
+ let actualMutedState = __classPrivateFieldGet(this, _YouTubeEmbed_muted, "f");
1112
+ if (this.player && this.playerReady) {
1113
+ try {
1114
+ actualMutedState = this.player.isMuted();
1115
+ // Sync internal state with actual player state
1116
+ if (actualMutedState !== __classPrivateFieldGet(this, _YouTubeEmbed_muted, "f")) {
1117
+ __classPrivateFieldSet(this, _YouTubeEmbed_muted, actualMutedState, "f");
1118
+ }
1119
+ }
1120
+ catch (error) {
1121
+ // Fallback to internal state if player method fails
1122
+ this.warn("YouTubeEmbed: Could not get muted state from player:", error);
1123
+ }
1124
+ }
1125
+ return {
1126
+ playing: __classPrivateFieldGet(this, _YouTubeEmbed_playing, "f"),
1127
+ muted: actualMutedState,
1128
+ videoId: __classPrivateFieldGet(this, _YouTubeEmbed_videoId, "f"),
1129
+ ready: this.playerReady,
1130
+ initialized: this.initialized,
1131
+ };
1132
+ }
1133
+ // Public API for checking if video is playing
1134
+ /**
1135
+ * Checks if the video is currently playing.
1136
+ * @returns `true` if the video is playing.
1137
+ */
1138
+ isPlaying() {
1139
+ return __classPrivateFieldGet(this, _YouTubeEmbed_playing, "f");
1140
+ }
1141
+ // Public API for checking if video is muted
1142
+ /**
1143
+ * Checks if the video is currently muted.
1144
+ * @returns `true` if the video is muted.
1145
+ */
1146
+ isMuted() {
1147
+ if (this.player && this.playerReady) {
1148
+ try {
1149
+ return this.player.isMuted();
1150
+ }
1151
+ catch (error) {
1152
+ this.warn("YouTubeEmbed: Could not get muted state from player:", error);
1153
+ }
1154
+ }
1155
+ return __classPrivateFieldGet(this, _YouTubeEmbed_muted, "f");
1156
+ }
1157
+ /**
1158
+ * Request fullscreen mode
1159
+ */
1160
+ enterFullscreen() {
1161
+ const elem = this;
1162
+ if (elem.requestFullscreen) {
1163
+ return elem.requestFullscreen();
1164
+ }
1165
+ else if (elem.webkitRequestFullscreen) {
1166
+ return elem.webkitRequestFullscreen();
1167
+ }
1168
+ else if (elem.mozRequestFullScreen) {
1169
+ return elem.mozRequestFullScreen();
1170
+ }
1171
+ else if (elem.msRequestFullscreen) {
1172
+ return elem.msRequestFullscreen();
1173
+ }
1174
+ return Promise.reject(new Error("Fullscreen API not supported"));
1175
+ }
1176
+ /**
1177
+ * Exit fullscreen mode
1178
+ */
1179
+ exitFullscreen() {
1180
+ const doc = document;
1181
+ if (doc.exitFullscreen) {
1182
+ return doc.exitFullscreen();
1183
+ }
1184
+ else if (doc.webkitExitFullscreen) {
1185
+ return doc.webkitExitFullscreen();
1186
+ }
1187
+ else if (doc.mozCancelFullScreen) {
1188
+ return doc.mozCancelFullScreen();
1189
+ }
1190
+ else if (doc.msExitFullscreen) {
1191
+ return doc.msExitFullscreen();
1192
+ }
1193
+ return Promise.reject(new Error("Fullscreen API not supported"));
1194
+ }
1195
+ /**
1196
+ * Toggle fullscreen mode
1197
+ */
1198
+ toggleFullscreen() {
1199
+ return __awaiter(this, void 0, void 0, function* () {
1200
+ if (this.isFullscreen()) {
1201
+ yield this.exitFullscreen();
1202
+ }
1203
+ else {
1204
+ yield this.enterFullscreen();
1205
+ }
1206
+ });
1207
+ }
1208
+ /**
1209
+ * Check if currently in fullscreen mode
1210
+ */
1211
+ isFullscreen() {
1212
+ const doc = document;
1213
+ return !!(doc.fullscreenElement ||
1214
+ doc.webkitFullscreenElement ||
1215
+ doc.mozFullScreenElement ||
1216
+ doc.msFullscreenElement);
1217
+ }
1218
+ /**
1219
+ * Get available quality levels for the current video
1220
+ * @returns Array of available quality levels
1221
+ */
1222
+ getAvailableQualities() {
1223
+ if (this.player && this.playerReady) {
1224
+ try {
1225
+ return this.player.getAvailableQualityLevels() || [];
1226
+ }
1227
+ catch (error) {
1228
+ this.warn("YouTubeEmbed: Could not get available qualities:", error);
1229
+ }
1230
+ }
1231
+ return [];
1232
+ }
1233
+ /**
1234
+ * Get the current playback quality
1235
+ * @returns Current quality level
1236
+ */
1237
+ getCurrentQuality() {
1238
+ if (this.player && this.playerReady) {
1239
+ try {
1240
+ return this.player.getPlaybackQuality() || "auto";
1241
+ }
1242
+ catch (error) {
1243
+ this.warn("YouTubeEmbed: Could not get current quality:", error);
1244
+ }
1245
+ }
1246
+ return "auto";
1247
+ }
1248
+ /**
1249
+ * Set the playback quality
1250
+ * @param quality The desired quality level
1251
+ */
1252
+ setQuality(quality) {
1253
+ if (!this.player || !this.playerReady) {
1254
+ this.warn("YouTubeEmbed: Player not ready for quality change");
1255
+ return;
1256
+ }
1257
+ try {
1258
+ const oldQuality = this.getCurrentQuality();
1259
+ this.player.setPlaybackQuality(quality);
1260
+ // Dispatch quality change event
1261
+ const event = new CustomEvent("qualitychange", {
1262
+ detail: {
1263
+ oldQuality,
1264
+ newQuality: quality,
1265
+ availableQualities: this.getAvailableQualities(),
1266
+ },
1267
+ bubbles: true,
1268
+ composed: true,
1269
+ });
1270
+ this.dispatchEvent(event);
1271
+ }
1272
+ catch (error) {
1273
+ this.warn("YouTubeEmbed: Could not set quality:", error);
1274
+ }
1275
+ }
1276
+ // Public API for getting current playback time
1277
+ /**
1278
+ * Gets the current playback time in seconds.
1279
+ * @returns The current time in seconds.
1280
+ */
1281
+ getCurrentTime() {
1282
+ if (this.player && this.playerReady) {
1283
+ try {
1284
+ return this.player.getCurrentTime() || 0;
1285
+ }
1286
+ catch (error) {
1287
+ this.warn("YouTubeEmbed: Could not get current time from player:", error);
1288
+ }
1289
+ }
1290
+ return 0;
1291
+ }
1292
+ // Public API for setting video by ID or URL
1293
+ /**
1294
+ * Loads a new video by its ID or URL.
1295
+ * @param videoIdOrUrl The 11-character video ID or the full YouTube URL.
1296
+ */
1297
+ loadVideo(videoIdOrUrl) {
1298
+ // Determine if input is a full URL or a plain video ID
1299
+ const extracted = this.extractVideoId(videoIdOrUrl);
1300
+ if (extracted) {
1301
+ __classPrivateFieldSet(this, _YouTubeEmbed_url, videoIdOrUrl, "f");
1302
+ __classPrivateFieldSet(this, _YouTubeEmbed_videoId, extracted, "f");
1303
+ }
1304
+ else {
1305
+ __classPrivateFieldSet(this, _YouTubeEmbed_videoId, videoIdOrUrl, "f");
1306
+ }
1307
+ // If component is lazy, load immediately when requested
1308
+ if (__classPrivateFieldGet(this, _YouTubeEmbed_lazy, "f") && !this.player) {
1309
+ try {
1310
+ this.initializePlayer(__classPrivateFieldGet(this, _YouTubeEmbed_videoId, "f"));
1311
+ }
1312
+ catch (error) {
1313
+ this.warn("YouTubeEmbed: loadVideo failed to initialize player:", error);
1314
+ }
1315
+ return;
1316
+ }
1317
+ // If already initialized, reinitialize player with the new video
1318
+ if (this.initialized) {
1319
+ this.reinitializePlayer();
1320
+ }
1321
+ }
1322
+ }
1323
+ _YouTubeEmbed_url = new WeakMap(), _YouTubeEmbed_videoId = new WeakMap(), _YouTubeEmbed_autoplay = new WeakMap(), _YouTubeEmbed_muted = new WeakMap(), _YouTubeEmbed_controls = new WeakMap(), _YouTubeEmbed_lazy = new WeakMap(), _YouTubeEmbed_poster = new WeakMap(), _YouTubeEmbed_playing = new WeakMap(), _YouTubeEmbed_background = new WeakMap(), _YouTubeEmbed_playerVars = new WeakMap(), _YouTubeEmbed_instances = new WeakSet(), _YouTubeEmbed_reflectBooleanAttribute = function _YouTubeEmbed_reflectBooleanAttribute(name, value) {
1324
+ this.updatingAttribute = true;
1325
+ if (value) {
1326
+ this.setAttribute(name, "");
1327
+ }
1328
+ else {
1329
+ this.removeAttribute(name);
1330
+ }
1331
+ this.updatingAttribute = false;
1332
+ }, _YouTubeEmbed_updateBackgroundMode = function _YouTubeEmbed_updateBackgroundMode() {
1333
+ if (__classPrivateFieldGet(this, _YouTubeEmbed_background, "f")) {
1334
+ this.classList.add("is-background");
1335
+ // Ensure properties for background mode are set
1336
+ __classPrivateFieldSet(this, _YouTubeEmbed_autoplay, true, "f");
1337
+ __classPrivateFieldSet(this, _YouTubeEmbed_muted, true, "f");
1338
+ __classPrivateFieldSet(this, _YouTubeEmbed_controls, false, "f");
1339
+ __classPrivateFieldSet(this, _YouTubeEmbed_lazy, false, "f"); // Background videos should load immediately
1340
+ }
1341
+ else {
1342
+ this.classList.remove("is-background");
1343
+ }
1344
+ };
1345
+ YouTubeEmbed.apiLoaded = false;
1346
+ YouTubeEmbed.apiReady = false;
1347
+ YouTubeEmbed.instanceCount = 0; // Track active instances
1348
+ YouTubeEmbed.DEBUG = false; // Enable/disable debug logging
1349
+ YouTubeEmbed.MAX_API_RETRIES = 3;
1350
+ YouTubeEmbed.API_RETRY_DELAY = 2000;
1351
+ YouTubeEmbed.API_LOAD_TIMEOUT = 10000;
1352
+ customElements.define("youtube-embed", YouTubeEmbed);
1353
+ // Component ready for testing
1354
+ //# sourceMappingURL=YouTubeEmbed.js.map