embed-manager 1.0.0 → 1.0.2

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();
@@ -233,12 +234,18 @@ class EmbedManager {
233
234
  case 'twitter':
234
235
  case 'x':
235
236
  // Twitter/X embeds need special handling with their widget.js
236
- // We'll return the src as is, but we need to load their script
237
237
  this.loadExternalScript('https://platform.twitter.com/widgets.js', 'twitter-widget');
238
238
 
239
- // If the source is just a tweet ID, construct the proper URL
240
239
  if (/^\d+$/.test(src)) {
240
+ // Bare numeric tweet ID
241
241
  finalSrc = `https://twitter.com/i/status/${src}`;
242
+ } else {
243
+ // Full URL (x.com or twitter.com) — extract status ID and
244
+ // normalize to twitter.com so widgets.js processes it reliably
245
+ const statusMatch = src.match(/\/status\/(\d+)/);
246
+ if (statusMatch) {
247
+ finalSrc = `https://twitter.com/i/status/${statusMatch[1]}`;
248
+ }
242
249
  }
243
250
  break;
244
251
 
@@ -427,8 +434,17 @@ class EmbedManager {
427
434
  handleSpecialEmbed(embed, type) {
428
435
  const src = embed.getAttribute('data-src');
429
436
  const title = embed.getAttribute('data-title') || 'Untitled Embed';
437
+ const timeoutMs = this.options.embedTimeout;
438
+
439
+ // twitter/x may use a plain numeric tweet ID instead of a full URL
440
+ if (type !== 'twitter' && type !== 'x') {
441
+ if (!src || !this.isValidUrl(src)) {
442
+ this.showError(embed, `Invalid ${type} source URL`);
443
+ return;
444
+ }
445
+ }
430
446
 
431
- // Show loading state
447
+ // Show loading placeholder
432
448
  const loadingMessage = document.createElement('div');
433
449
  loadingMessage.className = 'embed-placeholder';
434
450
  loadingMessage.setAttribute('aria-live', 'polite');
@@ -439,7 +455,7 @@ class EmbedManager {
439
455
  try {
440
456
  switch (type) {
441
457
  case 'twitter':
442
- case 'x':
458
+ case 'x': {
443
459
  // Create a blockquote for Twitter to transform
444
460
  const tweetUrl = this.buildEmbedSrc(embed, src, type);
445
461
  const tweetContainer = document.createElement('blockquote');
@@ -455,16 +471,24 @@ class EmbedManager {
455
471
  embed.innerHTML = '';
456
472
  embed.appendChild(tweetContainer);
457
473
 
458
- // Initialize Twitter widgets
459
474
  if (window.twttr && window.twttr.widgets) {
460
475
  window.twttr.widgets.load(embed);
461
476
  } else {
462
- // The script will auto-process when loaded
463
477
  this.loadExternalScript('https://platform.twitter.com/widgets.js', 'twitter-widget');
464
478
  }
479
+
480
+ // Twitter widget.js replaces the blockquote with an <iframe> on success
481
+ if (timeoutMs > 0) {
482
+ setTimeout(() => {
483
+ if (!embed.querySelector('iframe')) {
484
+ this.showError(embed, 'Tweet failed to load. Check that the URL is correct and the tweet is publicly accessible.');
485
+ }
486
+ }, timeoutMs);
487
+ }
465
488
  break;
489
+ }
466
490
 
467
- case 'instagram':
491
+ case 'instagram': {
468
492
  // Create an Instagram embed using blockquote format
469
493
  const instagramUrl = this.buildEmbedSrc(embed, src, type);
470
494
  const instagramContainer = document.createElement('blockquote');
@@ -476,7 +500,6 @@ class EmbedManager {
476
500
  instagramContainer.style.width = '100%';
477
501
  instagramContainer.style.maxWidth = '540px';
478
502
 
479
- // Add a link inside the blockquote (required for Instagram's script)
480
503
  const link = document.createElement('a');
481
504
  link.href = instagramUrl;
482
505
  link.textContent = title || 'View this post on Instagram';
@@ -486,25 +509,62 @@ class EmbedManager {
486
509
  embed.innerHTML = '';
487
510
  embed.appendChild(instagramContainer);
488
511
 
489
- // Load Instagram's embed script and process this container
490
512
  this.loadExternalScript('https://www.instagram.com/embed.js', 'instagram-embed');
491
513
 
492
- // Need to tell instgrm to process this embed
493
514
  if (window.instgrm) {
494
515
  window.instgrm.Embeds.process();
495
516
  }
517
+
518
+ // Instagram embed.js replaces the blockquote with an <iframe> on success
519
+ if (timeoutMs > 0) {
520
+ setTimeout(() => {
521
+ if (!embed.querySelector('iframe')) {
522
+ this.showError(embed, 'Instagram embed failed to load. Check that the URL is correct and the post is publicly accessible.');
523
+ }
524
+ }, timeoutMs);
525
+ }
496
526
  break;
527
+ }
497
528
 
498
529
  case 'gist':
499
- case 'github':
500
- // GitHub Gists use script tags
530
+ case 'github': {
501
531
  const gistUrl = this.buildEmbedSrc(embed, src, type);
502
- const gistScript = document.createElement('script');
503
- gistScript.src = gistUrl;
532
+
533
+ // Gist scripts use document.write(), which is blocked after page load.
534
+ // Using srcdoc gives the script a fresh document context to write into.
535
+ const iframe = document.createElement('iframe');
536
+ iframe.style.width = '100%';
537
+ iframe.style.border = 'none';
538
+ iframe.style.minHeight = '100px';
539
+ iframe.setAttribute('aria-label', title);
540
+ 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>`;
541
+
542
+ let settled = false;
543
+ let timeoutId = null;
544
+
545
+ iframe.addEventListener('load', () => {
546
+ settled = true;
547
+ if (timeoutId) clearTimeout(timeoutId);
548
+ embed.querySelector('.embed-placeholder')?.remove();
549
+ });
550
+ iframe.addEventListener('error', () => {
551
+ settled = true;
552
+ if (timeoutId) clearTimeout(timeoutId);
553
+ this.showError(embed, 'Failed to load GitHub Gist. Ensure the Gist is public and the URL is correct.');
554
+ });
555
+
556
+ if (timeoutMs > 0) {
557
+ timeoutId = setTimeout(() => {
558
+ if (!settled) {
559
+ this.showError(embed, 'GitHub Gist timed out. Ensure the Gist is public and the URL is correct.');
560
+ }
561
+ }, timeoutMs);
562
+ }
504
563
 
505
564
  embed.innerHTML = '';
506
- embed.appendChild(gistScript);
565
+ embed.appendChild(iframe);
507
566
  break;
567
+ }
508
568
  }
509
569
  } catch (error) {
510
570
  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",s="true"===t.getAttribute("data-preview");a.includes("/pen/")?a=a.replace("/pen/",s?"/embed/preview/":"/embed/"):s&&a.includes("/embed/")&&!a.includes("/embed/preview/")&&(a=a.replace("/embed/","/embed/preview/"));const n=a.includes("?")?"&":"?";a=`${a}${n}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"],s=t.getAttribute("data-app-id");s&&r.push(`app_id=${s}`),"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":if(this.loadExternalScript("https://platform.twitter.com/widgets.js","twitter-widget"),/^\d+$/.test(e))a=`https://twitter.com/i/status/${e}`;else{const t=e.match(/\/status\/(\d+)/);t&&(a=`https://twitter.com/i/status/${t[1]}`)}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",s="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=${s}&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%",s=t.getAttribute("data-height"),n=t.getAttribute("data-aspect-ratio")||"16/9";if(!i||!this.isValidUrl(i))return void this.showError(t,"Invalid embed source URL");s?(t.style.height=s,t.style.width=r,t.style.aspectRatio="unset"):(t.style.width=r,t.style.aspectRatio=n);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 s=document.createElement("div");s.className="embed-placeholder",s.setAttribute("aria-live","polite"),s.innerHTML=`<p>Loading ${e} content...</p>`,t.innerHTML="",t.appendChild(s);try{switch(e){case"twitter":case"x":{const s=this.buildEmbedSrc(t,i,e),n=document.createElement("blockquote");n.className="twitter-tweet",n.setAttribute("data-lang",t.getAttribute("data-lang")||"en"),n.setAttribute("data-theme",t.getAttribute("data-theme")||"light");const o=document.createElement("a");o.href=s,o.textContent=a,n.appendChild(o),t.innerHTML="",t.appendChild(n),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 s=this.buildEmbedSrc(t,i,e),n=document.createElement("blockquote");n.className="instagram-media",n.setAttribute("data-instgrm-captioned",""),n.setAttribute("data-instgrm-permalink",s),n.setAttribute("data-instgrm-version","14"),n.style.margin="0 auto",n.style.width="100%",n.style.maxWidth="540px";const o=document.createElement("a");o.href=s,o.textContent=a||"View this post on Instagram",o.target="_blank",n.appendChild(o),t.innerHTML="",t.appendChild(n),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 s=this.buildEmbedSrc(t,i,e),n=document.createElement("iframe");n.style.width="100%",n.style.border="none",n.style.minHeight="100px",n.setAttribute("aria-label",a),n.srcdoc=`<!DOCTYPE html><html><head><base target="_parent"><style>body{margin:0;font-family:sans-serif}</style></head><body><script src="${s}"><\/script></body></html>`;let o=!1,d=null;n.addEventListener("load",()=>{o=!0,d&&clearTimeout(d),t.querySelector(".embed-placeholder")?.remove()}),n.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(n);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.2",
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();
@@ -233,12 +234,18 @@ class EmbedManager {
233
234
  case 'twitter':
234
235
  case 'x':
235
236
  // Twitter/X embeds need special handling with their widget.js
236
- // We'll return the src as is, but we need to load their script
237
237
  this.loadExternalScript('https://platform.twitter.com/widgets.js', 'twitter-widget');
238
238
 
239
- // If the source is just a tweet ID, construct the proper URL
240
239
  if (/^\d+$/.test(src)) {
240
+ // Bare numeric tweet ID
241
241
  finalSrc = `https://twitter.com/i/status/${src}`;
242
+ } else {
243
+ // Full URL (x.com or twitter.com) — extract status ID and
244
+ // normalize to twitter.com so widgets.js processes it reliably
245
+ const statusMatch = src.match(/\/status\/(\d+)/);
246
+ if (statusMatch) {
247
+ finalSrc = `https://twitter.com/i/status/${statusMatch[1]}`;
248
+ }
242
249
  }
243
250
  break;
244
251
 
@@ -427,8 +434,17 @@ class EmbedManager {
427
434
  handleSpecialEmbed(embed, type) {
428
435
  const src = embed.getAttribute('data-src');
429
436
  const title = embed.getAttribute('data-title') || 'Untitled Embed';
437
+ const timeoutMs = this.options.embedTimeout;
438
+
439
+ // twitter/x may use a plain numeric tweet ID instead of a full URL
440
+ if (type !== 'twitter' && type !== 'x') {
441
+ if (!src || !this.isValidUrl(src)) {
442
+ this.showError(embed, `Invalid ${type} source URL`);
443
+ return;
444
+ }
445
+ }
430
446
 
431
- // Show loading state
447
+ // Show loading placeholder
432
448
  const loadingMessage = document.createElement('div');
433
449
  loadingMessage.className = 'embed-placeholder';
434
450
  loadingMessage.setAttribute('aria-live', 'polite');
@@ -439,7 +455,7 @@ class EmbedManager {
439
455
  try {
440
456
  switch (type) {
441
457
  case 'twitter':
442
- case 'x':
458
+ case 'x': {
443
459
  // Create a blockquote for Twitter to transform
444
460
  const tweetUrl = this.buildEmbedSrc(embed, src, type);
445
461
  const tweetContainer = document.createElement('blockquote');
@@ -455,16 +471,24 @@ class EmbedManager {
455
471
  embed.innerHTML = '';
456
472
  embed.appendChild(tweetContainer);
457
473
 
458
- // Initialize Twitter widgets
459
474
  if (window.twttr && window.twttr.widgets) {
460
475
  window.twttr.widgets.load(embed);
461
476
  } else {
462
- // The script will auto-process when loaded
463
477
  this.loadExternalScript('https://platform.twitter.com/widgets.js', 'twitter-widget');
464
478
  }
479
+
480
+ // Twitter widget.js replaces the blockquote with an <iframe> on success
481
+ if (timeoutMs > 0) {
482
+ setTimeout(() => {
483
+ if (!embed.querySelector('iframe')) {
484
+ this.showError(embed, 'Tweet failed to load. Check that the URL is correct and the tweet is publicly accessible.');
485
+ }
486
+ }, timeoutMs);
487
+ }
465
488
  break;
489
+ }
466
490
 
467
- case 'instagram':
491
+ case 'instagram': {
468
492
  // Create an Instagram embed using blockquote format
469
493
  const instagramUrl = this.buildEmbedSrc(embed, src, type);
470
494
  const instagramContainer = document.createElement('blockquote');
@@ -476,7 +500,6 @@ class EmbedManager {
476
500
  instagramContainer.style.width = '100%';
477
501
  instagramContainer.style.maxWidth = '540px';
478
502
 
479
- // Add a link inside the blockquote (required for Instagram's script)
480
503
  const link = document.createElement('a');
481
504
  link.href = instagramUrl;
482
505
  link.textContent = title || 'View this post on Instagram';
@@ -486,25 +509,62 @@ class EmbedManager {
486
509
  embed.innerHTML = '';
487
510
  embed.appendChild(instagramContainer);
488
511
 
489
- // Load Instagram's embed script and process this container
490
512
  this.loadExternalScript('https://www.instagram.com/embed.js', 'instagram-embed');
491
513
 
492
- // Need to tell instgrm to process this embed
493
514
  if (window.instgrm) {
494
515
  window.instgrm.Embeds.process();
495
516
  }
517
+
518
+ // Instagram embed.js replaces the blockquote with an <iframe> on success
519
+ if (timeoutMs > 0) {
520
+ setTimeout(() => {
521
+ if (!embed.querySelector('iframe')) {
522
+ this.showError(embed, 'Instagram embed failed to load. Check that the URL is correct and the post is publicly accessible.');
523
+ }
524
+ }, timeoutMs);
525
+ }
496
526
  break;
527
+ }
497
528
 
498
529
  case 'gist':
499
- case 'github':
500
- // GitHub Gists use script tags
530
+ case 'github': {
501
531
  const gistUrl = this.buildEmbedSrc(embed, src, type);
502
- const gistScript = document.createElement('script');
503
- gistScript.src = gistUrl;
532
+
533
+ // Gist scripts use document.write(), which is blocked after page load.
534
+ // Using srcdoc gives the script a fresh document context to write into.
535
+ const iframe = document.createElement('iframe');
536
+ iframe.style.width = '100%';
537
+ iframe.style.border = 'none';
538
+ iframe.style.minHeight = '100px';
539
+ iframe.setAttribute('aria-label', title);
540
+ 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>`;
541
+
542
+ let settled = false;
543
+ let timeoutId = null;
544
+
545
+ iframe.addEventListener('load', () => {
546
+ settled = true;
547
+ if (timeoutId) clearTimeout(timeoutId);
548
+ embed.querySelector('.embed-placeholder')?.remove();
549
+ });
550
+ iframe.addEventListener('error', () => {
551
+ settled = true;
552
+ if (timeoutId) clearTimeout(timeoutId);
553
+ this.showError(embed, 'Failed to load GitHub Gist. Ensure the Gist is public and the URL is correct.');
554
+ });
555
+
556
+ if (timeoutMs > 0) {
557
+ timeoutId = setTimeout(() => {
558
+ if (!settled) {
559
+ this.showError(embed, 'GitHub Gist timed out. Ensure the Gist is public and the URL is correct.');
560
+ }
561
+ }, timeoutMs);
562
+ }
504
563
 
505
564
  embed.innerHTML = '';
506
- embed.appendChild(gistScript);
565
+ embed.appendChild(iframe);
507
566
  break;
567
+ }
508
568
  }
509
569
  } catch (error) {
510
570
  this.showError(embed, error.message);