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.
- package/dist/embedManager.js +75 -15
- package/dist/embedManager.min.js +1 -1
- package/package.json +1 -1
- package/src/lib/embedManager.js +75 -15
package/dist/embedManager.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
503
|
-
|
|
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(
|
|
565
|
+
embed.appendChild(iframe);
|
|
507
566
|
break;
|
|
567
|
+
}
|
|
508
568
|
}
|
|
509
569
|
} catch (error) {
|
|
510
570
|
this.showError(embed, error.message);
|
package/dist/embedManager.min.js
CHANGED
|
@@ -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.
|
|
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",
|
package/src/lib/embedManager.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
503
|
-
|
|
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(
|
|
565
|
+
embed.appendChild(iframe);
|
|
507
566
|
break;
|
|
567
|
+
}
|
|
508
568
|
}
|
|
509
569
|
} catch (error) {
|
|
510
570
|
this.showError(embed, error.message);
|