embed-manager 1.0.0 → 1.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.
@@ -26,6 +26,7 @@ class EmbedManager {
26
26
  constructor(options = {}) {
27
27
  this.options = {
28
28
  rootMargin: '200px 0px',
29
+ embedTimeout: 15000, // ms before a special embed is declared failed
29
30
  ...options
30
31
  };
31
32
  this.injectCSS();
@@ -427,8 +428,17 @@ class EmbedManager {
427
428
  handleSpecialEmbed(embed, type) {
428
429
  const src = embed.getAttribute('data-src');
429
430
  const title = embed.getAttribute('data-title') || 'Untitled Embed';
431
+ const timeoutMs = this.options.embedTimeout;
430
432
 
431
- // Show loading state
433
+ // twitter/x may use a plain numeric tweet ID instead of a full URL
434
+ if (type !== 'twitter' && type !== 'x') {
435
+ if (!src || !this.isValidUrl(src)) {
436
+ this.showError(embed, `Invalid ${type} source URL`);
437
+ return;
438
+ }
439
+ }
440
+
441
+ // Show loading placeholder
432
442
  const loadingMessage = document.createElement('div');
433
443
  loadingMessage.className = 'embed-placeholder';
434
444
  loadingMessage.setAttribute('aria-live', 'polite');
@@ -439,7 +449,7 @@ class EmbedManager {
439
449
  try {
440
450
  switch (type) {
441
451
  case 'twitter':
442
- case 'x':
452
+ case 'x': {
443
453
  // Create a blockquote for Twitter to transform
444
454
  const tweetUrl = this.buildEmbedSrc(embed, src, type);
445
455
  const tweetContainer = document.createElement('blockquote');
@@ -455,16 +465,24 @@ class EmbedManager {
455
465
  embed.innerHTML = '';
456
466
  embed.appendChild(tweetContainer);
457
467
 
458
- // Initialize Twitter widgets
459
468
  if (window.twttr && window.twttr.widgets) {
460
469
  window.twttr.widgets.load(embed);
461
470
  } else {
462
- // The script will auto-process when loaded
463
471
  this.loadExternalScript('https://platform.twitter.com/widgets.js', 'twitter-widget');
464
472
  }
473
+
474
+ // Twitter widget.js replaces the blockquote with an <iframe> on success
475
+ if (timeoutMs > 0) {
476
+ setTimeout(() => {
477
+ if (!embed.querySelector('iframe')) {
478
+ this.showError(embed, 'Tweet failed to load. Check that the URL is correct and the tweet is publicly accessible.');
479
+ }
480
+ }, timeoutMs);
481
+ }
465
482
  break;
483
+ }
466
484
 
467
- case 'instagram':
485
+ case 'instagram': {
468
486
  // Create an Instagram embed using blockquote format
469
487
  const instagramUrl = this.buildEmbedSrc(embed, src, type);
470
488
  const instagramContainer = document.createElement('blockquote');
@@ -476,7 +494,6 @@ class EmbedManager {
476
494
  instagramContainer.style.width = '100%';
477
495
  instagramContainer.style.maxWidth = '540px';
478
496
 
479
- // Add a link inside the blockquote (required for Instagram's script)
480
497
  const link = document.createElement('a');
481
498
  link.href = instagramUrl;
482
499
  link.textContent = title || 'View this post on Instagram';
@@ -486,25 +503,62 @@ class EmbedManager {
486
503
  embed.innerHTML = '';
487
504
  embed.appendChild(instagramContainer);
488
505
 
489
- // Load Instagram's embed script and process this container
490
506
  this.loadExternalScript('https://www.instagram.com/embed.js', 'instagram-embed');
491
507
 
492
- // Need to tell instgrm to process this embed
493
508
  if (window.instgrm) {
494
509
  window.instgrm.Embeds.process();
495
510
  }
511
+
512
+ // Instagram embed.js replaces the blockquote with an <iframe> on success
513
+ if (timeoutMs > 0) {
514
+ setTimeout(() => {
515
+ if (!embed.querySelector('iframe')) {
516
+ this.showError(embed, 'Instagram embed failed to load. Check that the URL is correct and the post is publicly accessible.');
517
+ }
518
+ }, timeoutMs);
519
+ }
496
520
  break;
521
+ }
497
522
 
498
523
  case 'gist':
499
- case 'github':
500
- // GitHub Gists use script tags
524
+ case 'github': {
501
525
  const gistUrl = this.buildEmbedSrc(embed, src, type);
502
- const gistScript = document.createElement('script');
503
- gistScript.src = gistUrl;
526
+
527
+ // Gist scripts use document.write(), which is blocked after page load.
528
+ // Using srcdoc gives the script a fresh document context to write into.
529
+ const iframe = document.createElement('iframe');
530
+ iframe.style.width = '100%';
531
+ iframe.style.border = 'none';
532
+ iframe.style.minHeight = '100px';
533
+ iframe.setAttribute('aria-label', title);
534
+ iframe.srcdoc = `<!DOCTYPE html><html><head><base target="_parent"><style>body{margin:0;font-family:sans-serif}</style></head><body><script src="${gistUrl}"><\/script></body></html>`;
535
+
536
+ let settled = false;
537
+ let timeoutId = null;
538
+
539
+ iframe.addEventListener('load', () => {
540
+ settled = true;
541
+ if (timeoutId) clearTimeout(timeoutId);
542
+ embed.querySelector('.embed-placeholder')?.remove();
543
+ });
544
+ iframe.addEventListener('error', () => {
545
+ settled = true;
546
+ if (timeoutId) clearTimeout(timeoutId);
547
+ this.showError(embed, 'Failed to load GitHub Gist. Ensure the Gist is public and the URL is correct.');
548
+ });
549
+
550
+ if (timeoutMs > 0) {
551
+ timeoutId = setTimeout(() => {
552
+ if (!settled) {
553
+ this.showError(embed, 'GitHub Gist timed out. Ensure the Gist is public and the URL is correct.');
554
+ }
555
+ }, timeoutMs);
556
+ }
504
557
 
505
558
  embed.innerHTML = '';
506
- embed.appendChild(gistScript);
559
+ embed.appendChild(iframe);
507
560
  break;
561
+ }
508
562
  }
509
563
  } catch (error) {
510
564
  this.showError(embed, error.message);
@@ -1 +1 @@
1
- class EmbedManager{constructor(t={}){this.options={rootMargin:"200px 0px",...t},this.injectCSS(),this.init()}injectCSS(){const t=document.createElement("style");t.innerHTML="\n\t\t\t.embed-container {\n\t\t\t\tmargin: 20px auto;\n\t\t\t\tbackground: #f4f4f4;\n\t\t\t\tposition: relative;\n\t\t\t\toverflow: hidden;\n\t\t\t\tdisplay: flex;\n\t\t\t\tjustify-content: center;\n\t\t\t\talign-items: center;\n\t\t\t\t/* Default aspect ratio wrapper */\n\t\t\t\taspect-ratio: 16/9;\n\t\t\t}\n\t\t\t.embed-container iframe {\n\t\t\t\twidth: 100%;\n\t\t\t\theight: 100%;\n\t\t\t\tborder: none;\n\t\t\t\tdisplay: block;\n\t\t\t\tposition: absolute;\n\t\t\t\ttop: 0;\n\t\t\t\tleft: 0;\n\t\t\t}\n\t\t\t.embed-container p {\n\t\t\t\tmargin: 0;\n\t\t\t\tfont-size: 1em;\n\t\t\t\tcolor: #555;\n\t\t\t}\n\t\t\t.embed-container .embed-placeholder {\n\t\t\t\tdisplay: flex;\n\t\t\t\tflex-direction: column;\n\t\t\t\talign-items: center;\n\t\t\t\tjustify-content: center;\n\t\t\t\twidth: 100%;\n\t\t\t\theight: 100%;\n\t\t\t\ttext-align: center;\n\t\t\t\tpadding: 1rem;\n\t\t\t}\n\t\t\t.embed-container .embed-error {\n\t\t\t\tcolor: #721c24;\n\t\t\t\tbackground-color: #f8d7da;\n\t\t\t\tpadding: 0.75rem;\n\t\t\t\tborder-radius: 0.25rem;\n\t\t\t\tmargin: 0.5rem 0;\n\t\t\t\twidth: 100%;\n\t\t\t\ttext-align: center;\n\t\t\t}\n\t\t",document.head.appendChild(t)}init(){"loading"===document.readyState?document.addEventListener("DOMContentLoaded",()=>this.setupEmbeds()):this.setupEmbeds()}setupEmbeds(){const t=document.querySelectorAll(".embed-container");this.setupObserver(t)}setupObserver(t){const e=new IntersectionObserver(t=>{t.forEach(t=>{t.isIntersecting&&(this.lazyLoadEmbed(t.target),e.unobserve(t.target))})},{rootMargin:this.options.rootMargin});t.forEach(t=>{if(!t.innerHTML.trim()){const e=t.getAttribute("data-type")||"content",a=document.createElement("div");a.className="embed-placeholder",a.innerHTML=`<p>Loading ${e} content when visible</p>`,t.appendChild(a)}e.observe(t)})}showError(t,e){console.error(`EmbedManager Error: ${e}`),t.innerHTML=`<div class="embed-error" role="alert">${e}</div>`}isValidUrl(t){try{const e=new URL(t);return["https:","http:"].includes(e.protocol)}catch(t){return!1}}buildEmbedSrc(t,e,a){let i=e;switch(a){case"codepen":{const e=t.getAttribute("data-theme-id")||"",a=t.getAttribute("data-default-tab")||"result",n="true"===t.getAttribute("data-editable")?"true":"false",r="true"===t.getAttribute("data-preview");i.includes("/pen/")?i=i.replace("/pen/",r?"/embed/preview/":"/embed/"):r&&i.includes("/embed/")&&!i.includes("/embed/preview/")&&(i=i.replace("/embed/","/embed/preview/"));const s=i.includes("?")?"&":"?";i=`${i}${s}theme-id=${e}&default-tab=${a}&editable=${n}`;break}case"vimeo":{const a=t.getAttribute("data-hash");if(a&&!e.includes("h=")){const t=e.includes("?")?"&":"?";i=`${e}${t}h=${a}`}const n=["badge=0","autopause=0","player_id=0","dnt=1"],r=t.getAttribute("data-app-id");r&&n.push(`app_id=${r}`),"true"===t.getAttribute("data-autoplay")&&n.push("autoplay=1"),n.forEach(t=>{const e=t.split("=")[0];if(!i.includes(e+"=")){const e=i.includes("?")?"&":"?";i=`${i}${e}${t}`}});break}case"youtube":{const a=[];"true"===t.getAttribute("data-autoplay")&&a.push("autoplay=1"),e.includes("youtube-nocookie.com")||(i=i.replace("youtube.com","youtube-nocookie.com")),a.push("rel=0","modestbranding=1");const n=i.includes("?")?"&":"?";i=`${i}${n}${a.join("&")}`;break}case"twitch":{const t=window.location.hostname;i=`${i}&parent=${t}`;break}case"twitter":case"x":this.loadExternalScript("https://platform.twitter.com/widgets.js","twitter-widget"),/^\d+$/.test(e)&&(i=`https://twitter.com/i/status/${e}`);break;case"instagram":this.loadExternalScript("https://www.instagram.com/embed.js","instagram-embed"),(i.includes("instagram.com/p/")||i.includes("instagram.com/reel/"))&&(i.includes("?")?i.includes("utm_source=ig_embed")||(i=`${i}&utm_source=ig_embed&utm_campaign=loading`):i=`${i}?utm_source=ig_embed&utm_campaign=loading`);break;case"tiktok":if(this.loadExternalScript("https://www.tiktok.com/embed.js","tiktok-embed"),!i.includes("embed")){const t=i.replace(/\?.*$/,"").replace(/\/$/,"").split("/").pop();i=`https://www.tiktok.com/embed/v2/${t}`}break;case"soundcloud":if(!i.includes("api.soundcloud.com")){const a=t.getAttribute("data-color")||"ff5500",n="true"===t.getAttribute("data-autoplay")?"true":"false",r="true"===t.getAttribute("data-show-comments")?"true":"false";i=`https://w.soundcloud.com/player/?url=${encodeURIComponent(e)}&color=${a}&auto_play=${n}&hide_related=false&show_comments=${r}&show_user=true&show_reposts=false&show_teaser=true`}break;case"spotify":if(i.includes("spotify.com")){const t=i.includes("/track/")?"track":i.includes("/album/")?"album":i.includes("/playlist/")?"playlist":i.includes("/episode/")?"episode":"track",e=i.split("/").pop().split("?")[0];i=`https://open.spotify.com/embed/${t}/${e}`}break;case"github":case"gist":if(i.includes("gist.github.com")&&!i.endsWith(".js")){const t=i.split("/").pop();i=`https://gist.github.com/${t}.js`}break;case"maps":case"google-maps":if(!i.includes("google.com/maps/embed")){let e="";i.includes("maps/place/")?e=i.split("maps/place/")[1].split("/")[0]:i.includes("maps?q=")&&(e=i.split("maps?q=")[1].split("&")[0]),e&&(i=`https://www.google.com/maps/embed/v1/place?key=${t.getAttribute("data-api-key")||""}&q=${e}`)}}return i}loadExternalScript(t,e){if(!document.getElementById(e)){const a=document.createElement("script");a.id=e,a.src=t,a.async=!0,a.defer=!0,document.body.appendChild(a)}}lazyLoadEmbed(t){const e=t.getAttribute("data-type"),a=t.getAttribute("data-src");if("twitter"===e||"x"===e||"gist"===e||"github"===e||"instagram"===e)return void this.handleSpecialEmbed(t,e);const i=t.getAttribute("data-title")||"Untitled Embed",n=t.getAttribute("data-width")||"100%",r=t.getAttribute("data-height"),s=t.getAttribute("data-aspect-ratio")||"16/9";if(!a||!this.isValidUrl(a))return void this.showError(t,"Invalid embed source URL");r?(t.style.height=r,t.style.width=n,t.style.aspectRatio="unset"):(t.style.width=n,t.style.aspectRatio=s);const o=document.createElement("div");o.className="embed-placeholder",o.setAttribute("aria-live","polite"),o.innerHTML=`<p>Loading ${e} content...</p>`,t.innerHTML="",t.appendChild(o);const d=document.createElement("iframe");d.allow="autoplay; fullscreen; picture-in-picture; clipboard-write; encrypted-media",d.loading="lazy",d.title=i,d.setAttribute("allowfullscreen",""),d.setAttribute("aria-label",i),d.referrerPolicy="no-referrer-when-downgrade";try{let i=this.buildEmbedSrc(t,a,e);d.src=i,d.addEventListener("load",()=>{t.querySelector(".embed-placeholder")?.remove()}),d.addEventListener("error",()=>{this.showError(t,`Failed to load ${e} content`)}),"website"===e&&(d.sandbox="allow-scripts allow-same-origin allow-forms allow-popups"),t.appendChild(d)}catch(e){this.showError(t,e.message)}}handleSpecialEmbed(t,e){const a=t.getAttribute("data-src"),i=t.getAttribute("data-title")||"Untitled Embed",n=document.createElement("div");n.className="embed-placeholder",n.setAttribute("aria-live","polite"),n.innerHTML=`<p>Loading ${e} content...</p>`,t.innerHTML="",t.appendChild(n);try{switch(e){case"twitter":case"x":const n=this.buildEmbedSrc(t,a,e),r=document.createElement("blockquote");r.className="twitter-tweet",r.setAttribute("data-lang",t.getAttribute("data-lang")||"en"),r.setAttribute("data-theme",t.getAttribute("data-theme")||"light");const s=document.createElement("a");s.href=n,s.textContent=i,r.appendChild(s),t.innerHTML="",t.appendChild(r),window.twttr&&window.twttr.widgets?window.twttr.widgets.load(t):this.loadExternalScript("https://platform.twitter.com/widgets.js","twitter-widget");break;case"instagram":const o=this.buildEmbedSrc(t,a,e),d=document.createElement("blockquote");d.className="instagram-media",d.setAttribute("data-instgrm-captioned",""),d.setAttribute("data-instgrm-permalink",o),d.setAttribute("data-instgrm-version","14"),d.style.margin="0 auto",d.style.width="100%",d.style.maxWidth="540px";const c=document.createElement("a");c.href=o,c.textContent=i||"View this post on Instagram",c.target="_blank",d.appendChild(c),t.innerHTML="",t.appendChild(d),this.loadExternalScript("https://www.instagram.com/embed.js","instagram-embed"),window.instgrm&&window.instgrm.Embeds.process();break;case"gist":case"github":const l=this.buildEmbedSrc(t,a,e),m=document.createElement("script");m.src=l,t.innerHTML="",t.appendChild(m)}}catch(e){this.showError(t,e.message)}}processContainer(t){t&&t.classList.contains("embed-container")&&this.lazyLoadEmbed(t)}addEmbed(t){t&&t.classList.contains("embed-container")&&this.setupObserver([t])}}"undefined"!=typeof module&&module.exports&&(module.exports=EmbedManager),"undefined"!=typeof window&&"undefined"==typeof module&&(window.EmbedManager=new EmbedManager);
1
+ class EmbedManager{constructor(t={}){this.options={rootMargin:"200px 0px",embedTimeout:15e3,...t},this.injectCSS(),this.init()}injectCSS(){const t=document.createElement("style");t.innerHTML="\n\t\t\t.embed-container {\n\t\t\t\tmargin: 20px auto;\n\t\t\t\tbackground: #f4f4f4;\n\t\t\t\tposition: relative;\n\t\t\t\toverflow: hidden;\n\t\t\t\tdisplay: flex;\n\t\t\t\tjustify-content: center;\n\t\t\t\talign-items: center;\n\t\t\t\t/* Default aspect ratio wrapper */\n\t\t\t\taspect-ratio: 16/9;\n\t\t\t}\n\t\t\t.embed-container iframe {\n\t\t\t\twidth: 100%;\n\t\t\t\theight: 100%;\n\t\t\t\tborder: none;\n\t\t\t\tdisplay: block;\n\t\t\t\tposition: absolute;\n\t\t\t\ttop: 0;\n\t\t\t\tleft: 0;\n\t\t\t}\n\t\t\t.embed-container p {\n\t\t\t\tmargin: 0;\n\t\t\t\tfont-size: 1em;\n\t\t\t\tcolor: #555;\n\t\t\t}\n\t\t\t.embed-container .embed-placeholder {\n\t\t\t\tdisplay: flex;\n\t\t\t\tflex-direction: column;\n\t\t\t\talign-items: center;\n\t\t\t\tjustify-content: center;\n\t\t\t\twidth: 100%;\n\t\t\t\theight: 100%;\n\t\t\t\ttext-align: center;\n\t\t\t\tpadding: 1rem;\n\t\t\t}\n\t\t\t.embed-container .embed-error {\n\t\t\t\tcolor: #721c24;\n\t\t\t\tbackground-color: #f8d7da;\n\t\t\t\tpadding: 0.75rem;\n\t\t\t\tborder-radius: 0.25rem;\n\t\t\t\tmargin: 0.5rem 0;\n\t\t\t\twidth: 100%;\n\t\t\t\ttext-align: center;\n\t\t\t}\n\t\t",document.head.appendChild(t)}init(){"loading"===document.readyState?document.addEventListener("DOMContentLoaded",()=>this.setupEmbeds()):this.setupEmbeds()}setupEmbeds(){const t=document.querySelectorAll(".embed-container");this.setupObserver(t)}setupObserver(t){const e=new IntersectionObserver(t=>{t.forEach(t=>{t.isIntersecting&&(this.lazyLoadEmbed(t.target),e.unobserve(t.target))})},{rootMargin:this.options.rootMargin});t.forEach(t=>{if(!t.innerHTML.trim()){const e=t.getAttribute("data-type")||"content",i=document.createElement("div");i.className="embed-placeholder",i.innerHTML=`<p>Loading ${e} content when visible</p>`,t.appendChild(i)}e.observe(t)})}showError(t,e){console.error(`EmbedManager Error: ${e}`),t.innerHTML=`<div class="embed-error" role="alert">${e}</div>`}isValidUrl(t){try{const e=new URL(t);return["https:","http:"].includes(e.protocol)}catch(t){return!1}}buildEmbedSrc(t,e,i){let a=e;switch(i){case"codepen":{const e=t.getAttribute("data-theme-id")||"",i=t.getAttribute("data-default-tab")||"result",r="true"===t.getAttribute("data-editable")?"true":"false",n="true"===t.getAttribute("data-preview");a.includes("/pen/")?a=a.replace("/pen/",n?"/embed/preview/":"/embed/"):n&&a.includes("/embed/")&&!a.includes("/embed/preview/")&&(a=a.replace("/embed/","/embed/preview/"));const s=a.includes("?")?"&":"?";a=`${a}${s}theme-id=${e}&default-tab=${i}&editable=${r}`;break}case"vimeo":{const i=t.getAttribute("data-hash");if(i&&!e.includes("h=")){const t=e.includes("?")?"&":"?";a=`${e}${t}h=${i}`}const r=["badge=0","autopause=0","player_id=0","dnt=1"],n=t.getAttribute("data-app-id");n&&r.push(`app_id=${n}`),"true"===t.getAttribute("data-autoplay")&&r.push("autoplay=1"),r.forEach(t=>{const e=t.split("=")[0];if(!a.includes(e+"=")){const e=a.includes("?")?"&":"?";a=`${a}${e}${t}`}});break}case"youtube":{const i=[];"true"===t.getAttribute("data-autoplay")&&i.push("autoplay=1"),e.includes("youtube-nocookie.com")||(a=a.replace("youtube.com","youtube-nocookie.com")),i.push("rel=0","modestbranding=1");const r=a.includes("?")?"&":"?";a=`${a}${r}${i.join("&")}`;break}case"twitch":{const t=window.location.hostname;a=`${a}&parent=${t}`;break}case"twitter":case"x":this.loadExternalScript("https://platform.twitter.com/widgets.js","twitter-widget"),/^\d+$/.test(e)&&(a=`https://twitter.com/i/status/${e}`);break;case"instagram":this.loadExternalScript("https://www.instagram.com/embed.js","instagram-embed"),(a.includes("instagram.com/p/")||a.includes("instagram.com/reel/"))&&(a.includes("?")?a.includes("utm_source=ig_embed")||(a=`${a}&utm_source=ig_embed&utm_campaign=loading`):a=`${a}?utm_source=ig_embed&utm_campaign=loading`);break;case"tiktok":if(this.loadExternalScript("https://www.tiktok.com/embed.js","tiktok-embed"),!a.includes("embed")){const t=a.replace(/\?.*$/,"").replace(/\/$/,"").split("/").pop();a=`https://www.tiktok.com/embed/v2/${t}`}break;case"soundcloud":if(!a.includes("api.soundcloud.com")){const i=t.getAttribute("data-color")||"ff5500",r="true"===t.getAttribute("data-autoplay")?"true":"false",n="true"===t.getAttribute("data-show-comments")?"true":"false";a=`https://w.soundcloud.com/player/?url=${encodeURIComponent(e)}&color=${i}&auto_play=${r}&hide_related=false&show_comments=${n}&show_user=true&show_reposts=false&show_teaser=true`}break;case"spotify":if(a.includes("spotify.com")){const t=a.includes("/track/")?"track":a.includes("/album/")?"album":a.includes("/playlist/")?"playlist":a.includes("/episode/")?"episode":"track",e=a.split("/").pop().split("?")[0];a=`https://open.spotify.com/embed/${t}/${e}`}break;case"github":case"gist":if(a.includes("gist.github.com")&&!a.endsWith(".js")){const t=a.split("/").pop();a=`https://gist.github.com/${t}.js`}break;case"maps":case"google-maps":if(!a.includes("google.com/maps/embed")){let e="";a.includes("maps/place/")?e=a.split("maps/place/")[1].split("/")[0]:a.includes("maps?q=")&&(e=a.split("maps?q=")[1].split("&")[0]),e&&(a=`https://www.google.com/maps/embed/v1/place?key=${t.getAttribute("data-api-key")||""}&q=${e}`)}}return a}loadExternalScript(t,e){if(!document.getElementById(e)){const i=document.createElement("script");i.id=e,i.src=t,i.async=!0,i.defer=!0,document.body.appendChild(i)}}lazyLoadEmbed(t){const e=t.getAttribute("data-type"),i=t.getAttribute("data-src");if("twitter"===e||"x"===e||"gist"===e||"github"===e||"instagram"===e)return void this.handleSpecialEmbed(t,e);const a=t.getAttribute("data-title")||"Untitled Embed",r=t.getAttribute("data-width")||"100%",n=t.getAttribute("data-height"),s=t.getAttribute("data-aspect-ratio")||"16/9";if(!i||!this.isValidUrl(i))return void this.showError(t,"Invalid embed source URL");n?(t.style.height=n,t.style.width=r,t.style.aspectRatio="unset"):(t.style.width=r,t.style.aspectRatio=s);const o=document.createElement("div");o.className="embed-placeholder",o.setAttribute("aria-live","polite"),o.innerHTML=`<p>Loading ${e} content...</p>`,t.innerHTML="",t.appendChild(o);const d=document.createElement("iframe");d.allow="autoplay; fullscreen; picture-in-picture; clipboard-write; encrypted-media",d.loading="lazy",d.title=a,d.setAttribute("allowfullscreen",""),d.setAttribute("aria-label",a),d.referrerPolicy="no-referrer-when-downgrade";try{let a=this.buildEmbedSrc(t,i,e);d.src=a,d.addEventListener("load",()=>{t.querySelector(".embed-placeholder")?.remove()}),d.addEventListener("error",()=>{this.showError(t,`Failed to load ${e} content`)}),"website"===e&&(d.sandbox="allow-scripts allow-same-origin allow-forms allow-popups"),t.appendChild(d)}catch(e){this.showError(t,e.message)}}handleSpecialEmbed(t,e){const i=t.getAttribute("data-src"),a=t.getAttribute("data-title")||"Untitled Embed",r=this.options.embedTimeout;if(!("twitter"===e||"x"===e||i&&this.isValidUrl(i)))return void this.showError(t,`Invalid ${e} source URL`);const n=document.createElement("div");n.className="embed-placeholder",n.setAttribute("aria-live","polite"),n.innerHTML=`<p>Loading ${e} content...</p>`,t.innerHTML="",t.appendChild(n);try{switch(e){case"twitter":case"x":{const n=this.buildEmbedSrc(t,i,e),s=document.createElement("blockquote");s.className="twitter-tweet",s.setAttribute("data-lang",t.getAttribute("data-lang")||"en"),s.setAttribute("data-theme",t.getAttribute("data-theme")||"light");const o=document.createElement("a");o.href=n,o.textContent=a,s.appendChild(o),t.innerHTML="",t.appendChild(s),window.twttr&&window.twttr.widgets?window.twttr.widgets.load(t):this.loadExternalScript("https://platform.twitter.com/widgets.js","twitter-widget"),r>0&&setTimeout(()=>{t.querySelector("iframe")||this.showError(t,"Tweet failed to load. Check that the URL is correct and the tweet is publicly accessible.")},r);break}case"instagram":{const n=this.buildEmbedSrc(t,i,e),s=document.createElement("blockquote");s.className="instagram-media",s.setAttribute("data-instgrm-captioned",""),s.setAttribute("data-instgrm-permalink",n),s.setAttribute("data-instgrm-version","14"),s.style.margin="0 auto",s.style.width="100%",s.style.maxWidth="540px";const o=document.createElement("a");o.href=n,o.textContent=a||"View this post on Instagram",o.target="_blank",s.appendChild(o),t.innerHTML="",t.appendChild(s),this.loadExternalScript("https://www.instagram.com/embed.js","instagram-embed"),window.instgrm&&window.instgrm.Embeds.process(),r>0&&setTimeout(()=>{t.querySelector("iframe")||this.showError(t,"Instagram embed failed to load. Check that the URL is correct and the post is publicly accessible.")},r);break}case"gist":case"github":{const n=this.buildEmbedSrc(t,i,e),s=document.createElement("iframe");s.style.width="100%",s.style.border="none",s.style.minHeight="100px",s.setAttribute("aria-label",a),s.srcdoc=`<!DOCTYPE html><html><head><base target="_parent"><style>body{margin:0;font-family:sans-serif}</style></head><body><script src="${n}"><\/script></body></html>`;let o=!1,d=null;s.addEventListener("load",()=>{o=!0,d&&clearTimeout(d),t.querySelector(".embed-placeholder")?.remove()}),s.addEventListener("error",()=>{o=!0,d&&clearTimeout(d),this.showError(t,"Failed to load GitHub Gist. Ensure the Gist is public and the URL is correct.")}),r>0&&(d=setTimeout(()=>{o||this.showError(t,"GitHub Gist timed out. Ensure the Gist is public and the URL is correct.")},r)),t.innerHTML="",t.appendChild(s);break}}}catch(e){this.showError(t,e.message)}}processContainer(t){t&&t.classList.contains("embed-container")&&this.lazyLoadEmbed(t)}addEmbed(t){t&&t.classList.contains("embed-container")&&this.setupObserver([t])}}"undefined"!=typeof module&&module.exports&&(module.exports=EmbedManager),"undefined"!=typeof window&&"undefined"==typeof module&&(window.EmbedManager=new EmbedManager);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "embed-manager",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "description": "A versatile JavaScript library for embedding various content types (YouTube, Vimeo, Twitch, CodePen, and websites) with lazy loading capabilities",
5
5
  "main": "src/lib/embedManager.js",
6
6
  "browser": "dist/embedManager.min.js",
@@ -26,6 +26,7 @@ class EmbedManager {
26
26
  constructor(options = {}) {
27
27
  this.options = {
28
28
  rootMargin: '200px 0px',
29
+ embedTimeout: 15000, // ms before a special embed is declared failed
29
30
  ...options
30
31
  };
31
32
  this.injectCSS();
@@ -427,8 +428,17 @@ class EmbedManager {
427
428
  handleSpecialEmbed(embed, type) {
428
429
  const src = embed.getAttribute('data-src');
429
430
  const title = embed.getAttribute('data-title') || 'Untitled Embed';
431
+ const timeoutMs = this.options.embedTimeout;
430
432
 
431
- // Show loading state
433
+ // twitter/x may use a plain numeric tweet ID instead of a full URL
434
+ if (type !== 'twitter' && type !== 'x') {
435
+ if (!src || !this.isValidUrl(src)) {
436
+ this.showError(embed, `Invalid ${type} source URL`);
437
+ return;
438
+ }
439
+ }
440
+
441
+ // Show loading placeholder
432
442
  const loadingMessage = document.createElement('div');
433
443
  loadingMessage.className = 'embed-placeholder';
434
444
  loadingMessage.setAttribute('aria-live', 'polite');
@@ -439,7 +449,7 @@ class EmbedManager {
439
449
  try {
440
450
  switch (type) {
441
451
  case 'twitter':
442
- case 'x':
452
+ case 'x': {
443
453
  // Create a blockquote for Twitter to transform
444
454
  const tweetUrl = this.buildEmbedSrc(embed, src, type);
445
455
  const tweetContainer = document.createElement('blockquote');
@@ -455,16 +465,24 @@ class EmbedManager {
455
465
  embed.innerHTML = '';
456
466
  embed.appendChild(tweetContainer);
457
467
 
458
- // Initialize Twitter widgets
459
468
  if (window.twttr && window.twttr.widgets) {
460
469
  window.twttr.widgets.load(embed);
461
470
  } else {
462
- // The script will auto-process when loaded
463
471
  this.loadExternalScript('https://platform.twitter.com/widgets.js', 'twitter-widget');
464
472
  }
473
+
474
+ // Twitter widget.js replaces the blockquote with an <iframe> on success
475
+ if (timeoutMs > 0) {
476
+ setTimeout(() => {
477
+ if (!embed.querySelector('iframe')) {
478
+ this.showError(embed, 'Tweet failed to load. Check that the URL is correct and the tweet is publicly accessible.');
479
+ }
480
+ }, timeoutMs);
481
+ }
465
482
  break;
483
+ }
466
484
 
467
- case 'instagram':
485
+ case 'instagram': {
468
486
  // Create an Instagram embed using blockquote format
469
487
  const instagramUrl = this.buildEmbedSrc(embed, src, type);
470
488
  const instagramContainer = document.createElement('blockquote');
@@ -476,7 +494,6 @@ class EmbedManager {
476
494
  instagramContainer.style.width = '100%';
477
495
  instagramContainer.style.maxWidth = '540px';
478
496
 
479
- // Add a link inside the blockquote (required for Instagram's script)
480
497
  const link = document.createElement('a');
481
498
  link.href = instagramUrl;
482
499
  link.textContent = title || 'View this post on Instagram';
@@ -486,25 +503,62 @@ class EmbedManager {
486
503
  embed.innerHTML = '';
487
504
  embed.appendChild(instagramContainer);
488
505
 
489
- // Load Instagram's embed script and process this container
490
506
  this.loadExternalScript('https://www.instagram.com/embed.js', 'instagram-embed');
491
507
 
492
- // Need to tell instgrm to process this embed
493
508
  if (window.instgrm) {
494
509
  window.instgrm.Embeds.process();
495
510
  }
511
+
512
+ // Instagram embed.js replaces the blockquote with an <iframe> on success
513
+ if (timeoutMs > 0) {
514
+ setTimeout(() => {
515
+ if (!embed.querySelector('iframe')) {
516
+ this.showError(embed, 'Instagram embed failed to load. Check that the URL is correct and the post is publicly accessible.');
517
+ }
518
+ }, timeoutMs);
519
+ }
496
520
  break;
521
+ }
497
522
 
498
523
  case 'gist':
499
- case 'github':
500
- // GitHub Gists use script tags
524
+ case 'github': {
501
525
  const gistUrl = this.buildEmbedSrc(embed, src, type);
502
- const gistScript = document.createElement('script');
503
- gistScript.src = gistUrl;
526
+
527
+ // Gist scripts use document.write(), which is blocked after page load.
528
+ // Using srcdoc gives the script a fresh document context to write into.
529
+ const iframe = document.createElement('iframe');
530
+ iframe.style.width = '100%';
531
+ iframe.style.border = 'none';
532
+ iframe.style.minHeight = '100px';
533
+ iframe.setAttribute('aria-label', title);
534
+ iframe.srcdoc = `<!DOCTYPE html><html><head><base target="_parent"><style>body{margin:0;font-family:sans-serif}</style></head><body><script src="${gistUrl}"><\/script></body></html>`;
535
+
536
+ let settled = false;
537
+ let timeoutId = null;
538
+
539
+ iframe.addEventListener('load', () => {
540
+ settled = true;
541
+ if (timeoutId) clearTimeout(timeoutId);
542
+ embed.querySelector('.embed-placeholder')?.remove();
543
+ });
544
+ iframe.addEventListener('error', () => {
545
+ settled = true;
546
+ if (timeoutId) clearTimeout(timeoutId);
547
+ this.showError(embed, 'Failed to load GitHub Gist. Ensure the Gist is public and the URL is correct.');
548
+ });
549
+
550
+ if (timeoutMs > 0) {
551
+ timeoutId = setTimeout(() => {
552
+ if (!settled) {
553
+ this.showError(embed, 'GitHub Gist timed out. Ensure the Gist is public and the URL is correct.');
554
+ }
555
+ }, timeoutMs);
556
+ }
504
557
 
505
558
  embed.innerHTML = '';
506
- embed.appendChild(gistScript);
559
+ embed.appendChild(iframe);
507
560
  break;
561
+ }
508
562
  }
509
563
  } catch (error) {
510
564
  this.showError(embed, error.message);