Skip to content

Instantly share code, notes, and snippets.

@shgysk8zer0
Last active December 2, 2021 08:00
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save shgysk8zer0/68ee21d1d791754fb054dad0fbd7abda to your computer and use it in GitHub Desktop.
Save shgysk8zer0/68ee21d1d791754fb054dad0fbd7abda to your computer and use it in GitHub Desktop.
<github-gist> class for lazy-loading Gists

<github-gist> Instructions

This is a custom element / web component built using vanilla JavaScript to embed GistHub Gists into a webpage without having to worry about the inline scripts which call document.write(). It can be configured to lazy-load using loading="lazy" and styled with CSS through the github-gist & github-gist::part(embed) selectors.

Under-the-hood it creates an <iframe> using:

<iframe srcdoc="..." sandbox="allow-scripts allow-popups" width="..." height="..." referrerpolicy="no-referrer"></iframe>

Since it uses srcdoc, no additional requests are made beyond what the Gist embedding script would otherwise have loaded, but this method does isolate the Gist from the rest of the page.

Note: setting height or width will be overridden if ::part(emebed) sets either by CSS.

Usage (external assets)

index.html

<script type="module" src="/main.js"></script>
<link rel="stylesheet" href="styles.css">
<github-gist user="shgysk8zer0" gist="68ee21d1d791754fb054dad0fbd7abda" loading="lazy">
  <p>This content displays when the browser does not support custom elements.</p>
</github-gist>

main.js

import '/path/to/github-gist.js';

customElements.whenDefined('github-gist').then(/*...*/);

styles.css

/* Do not display until custom element has been defined */
github-gist:not(:defined) {
  display: none;
}

/* Style the <iframe> withing the Shadow root */
github-gist::part(embed) {
  max-width: 100%;
}

Usage (inline)

index.html

<!-- Still need to load the script -->
<script src="/path/to/github-gist.js" defer></script>
<style>
  github-gist::part(embed) {
    max-width: 100%;
  }
</style>
<github-gist user="shgysk8zer0" gist="68ee21d1d791754fb054dad0fbd7abda" loading="lazy">
  <p>This content displays when the browser does not support custom elements.</p>
</github-gist>

Usage (JavaScript only)

import '/path/to/github-gist.js';

customElements.whenDefined('github-gist').then(() => {
  const HTMLGitHubGistElement = customElements.get('github-gist');
  const gist = new HTMLGitHubGistElement();
  gist.loading = 'lazy';
  gist.user = 'shgysk8zer0';
  gist.gist = '68ee21d1d791754fb054dad0fbd7abda';
  gist.width = 800;
  gist.height = 500;
  
  docuument.querySelector('.gist-container').append(gist);
});

Optional attributes:

  • loading: lazy or eager, determining whether or not to lazy-load (defaults to eager)
  • height: Integer to set the height of the <iframe> in pixels
  • width: Integer to set the width of the <iframe> in pixels
  • file: Optionally display only a single file from a Gist by setting this to the filename

Helpful links:

const protectedData = new WeakMap();
async function render(target) {
const { shadow, timeout } = protectedData.get(target);
if (Number.isInteger(timeout)) {
cancelAnimationFrame(timeout);
protectedData.set(target, { shadow, timeout: null });
}
protectedData.set(target, {
timeout: requestAnimationFrame(() => {
const { user, gist, file, height, width } = target;
const iframe = document.createElement('iframe');
const script = document.createElement('script');
const secondScript = document.createElement('script');
const link = document.createElement('link');
const src = new URL(`/${user}/${gist}.js`, 'https://gist.github.com');
link.rel = 'preconnect';
link.href = 'https://github.githubassets.com';
if (typeof file === 'string' && file.length !== 0) {
src.searchParams.set('file', file);
}
script.src = src.href;
secondScript.text = 'document.querySelectorAll("a").forEach(function(a){a.target="_blank"});';
iframe.referrerPolicy = 'no-referrer';
iframe.sandbox.add('allow-scripts', 'allow-popups');
iframe.frameBorder = 0;
if (! Number.isNaN(width)) {
iframe.width = width;
}
if (! Number.isNaN(height)) {
iframe.height = height;
}
if ('part' in iframe) {
iframe.part.add('embed');
}
iframe.srcdoc = `<!DOCTYPE html><html><head>${link.outerHTML}</head><body>${script.outerHTML}${secondScript.outerHTML}</body></html>`;
shadow.replaceChildren(iframe);
target.dispatchEvent(new Event('rendered'));
protectedData.set(target, { shadow, timeout: null });
}),
shadow,
}, 100);
}
const observer = new IntersectionObserver((entries, observer) => {
entries.forEach(({ target, isIntersecting }) => {
if (isIntersecting && protectedData.has(target)) {
observer.unobserve(target);
render(target);
}
});
}, {
rootMargin: `${Math.floor(0.5 * Math.max(screen.height, 200))}px`,
});
customElements.define('github-gist', class HTMLGitHubGistElement extends HTMLElement {
constructor() {
super();
const shadow = this.attachShadow({ mode: 'closed' });
protectedData.set(this, { shadow, timeout: null });
}
connectedCallback() {
this.dispatchEvent(new Event('connected'));
}
async attributeChangedCallback(name, oldValue, newValue) {
if (oldValue !== newValue) {
await this.whenConnected;
switch(name) {
case 'loading':
if (protectedData.get(this).shadow.childElementCount === 0) {
if (newValue === 'lazy') {
observer.observe(this);
} else {
observer.unobserve(this);
render(this);
}
}
break;
case 'user':
case 'gist':
case 'file':
if (this.loading !== 'lazy') {
render(this);
}
break;
case 'width':
case 'height':
this.rendered.then(() => {
const iframe = protectedData.get(this).shadow.querySelector('iframe');
if (typeof newValue === 'string') {
iframe.setAttribute(name, newValue);
} else {
iframe.removeAttribute(name);
}
});
break;
default:
throw new DOMException(`Unhandled attribute changed: ${name}`);
}
}
}
get file() {
return this.getAttribute('file');
}
set file(val) {
if (typeof val === 'string' && val.length !== 0) {
this.setAttribute('file', val);
} else {
this.removeAttribute('file');
}
}
get gist() {
return this.getAttribute('gist');
}
set gist(val) {
if (typeof val === 'string' && val.length !== 0) {
this.setAttribute('gist', val);
} else {
this.removeAttribute('gist');
}
}
get height() {
return parseInt(this.getAttribute('height'));
}
set height(val) {
if (Number.isSafeInteger(val) && val > 0) {
this.setAttribute('height', val);
} else {
this.removeAttribute('height');
}
}
get loading() {
return this.getAttribute('loading') || 'eager';
}
set loading(val) {
if (typeof val === 'string' && val.length !== 0) {
this.setAttribute('loading', val);
} else {
this.removeAttribute('val');
}
}
get user() {
return this.getAttribute('user');
}
set user(val) {
if (typeof val === 'string' && val.length !== 0) {
this.setAttribute('user', val);
} else {
this.removeAtttribute('user');
}
}
get rendered() {
return new Promise(async resolve => {
await this.whenConnected;
const { shadow } = protectedData.get(this);
if (shadow.childElementCount === 0) {
this.addEventListener('rendered', () => resolve(), { once: true });
} else {
resolve();
}
});
}
get width() {
return parseInt(this.getAttribute('width'));
}
set width(val) {
if (Number.isSafeInteger(val) && val > 0) {
this.setAttribute('width', val);
} else {
this.removeAttribute('width');
}
}
get whenConnected() {
return new Promise(resolve => {
if (this.isConnected) {
resolve();
} else {
this.addEventListener('connected', () => resolve(), { once: true });
}
});
}
static get observedAttributes() {
return ['user', 'gist', 'file', 'loading', 'width', 'height'];
}
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment