Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save abodacs/b41f53102bda2c697bd1e28e5d9cab0b to your computer and use it in GitHub Desktop.
Save abodacs/b41f53102bda2c697bd1e28e5d9cab0b to your computer and use it in GitHub Desktop.
Async CSS and Fonts DOM injection ES6 library
/*
* Whole module based on https://github.com/filamentgroup/loadCSS library
*/
/*
* Object with all css file hrefs with proper hash for cache invalidation,
* for all pages, made by Gulp from manifest.json at build time.
*/
import { css } from "./manifest-json/css.js";
/*
* Object with css font hrefs with proper hash for cache invalidation,
* made by Gulp from manifest.json at build time.
*/
import { fonts } from "./manifest-json/fonts.js";
/*
* Parse objects, imported above.
*/
const parseCssManifest = string =>
`css/${css.filter(item =>
item[string])[0][string]}`;
const parseFontsManifest = string =>
fonts
.filter(font =>
~Object.keys(font)[0].indexOf(string))[0][`fonts/${string}.css`];
/*
* Type checking.
*/
const isUndefined = (o) => typeof o === "undefined";
/*
* Debounce function. In this case it executes a wrapped function immediate,
* and do not allow function to execute again, until provided delay is finished.
*/
const debounce = function (func, wait, immediate = true) {
let timeout;
return function() {
const context = this, args = arguments;
const later = function() {
timeout = null;
immediate || func.apply(context, args);
};
const callNow = immediate && !timeout;
clearTimeout(timeout);
timeout = setTimeout(later, wait);
callNow && func.apply(context, args);
};
};
/*
* Once function executes wrapped function immediate,
* and swaps it with empty function body.
*/
function once (fn, context, ...args) {
let result;
return function() {
fn && (
result = fn.apply(context || this, args),
fn = () => {}
);
return result;
};
}
/*
* 100% location base, to prefix all parsed css links,
* before it could be compared with stylesheet.href.
*/
const baseLocation = `${location.protocol}//${location.hostname}${location.port ? ":" + location.port : ""}/`,
/*
* Simple browser detection.
*/
let isChromium = window.chrome,
isOpera = ~window.navigator.userAgent.indexOf("OPR");
/*
* returns the Object, with page css file links, to fetch proper one, and choose correct @media.
*/
const responsivePaths = pageName => ({
mobile: `${pageName}-mobile.css`,
tablet: `${pageName}-tablet.css`,
desktop: `${pageName}-desktop.css`
});
/*
* select proper font name extension for different browsers
*/
const fontTypeSupported = () =>
((isChromium !== null
&&
isChromium !== void 0
&&
window.navigator.vendor === "Google Inc."
&&
isOpera === false)
||
typeof InstallTrigger !== void 0
||
isOpera)
? "woff2"
: (Object.prototype.toString.call(window.HTMLElement).indexOf("Constructor") > 0
? "svg"
: "woff"
);
/*
* depends on browser width, choosing proper @media.
*/
const media = width =>
(width <= 767
? "(max-width:767px)"
: (width > 767 && width < 1200)
? "(min-width:768px) and (max-width:1199px)"
: "(min-width:1200px)"
);
/*
* Check, if link with href already exists in the DOM.
*/
const linkExists = href => Object.keys(sheets).filter(sheet => sheets[sheet].href === href).length ? href : void 0;
/*
* Asynk Appending fonts to the DOM.
*
* Chrome has a bug, which in case of changing link.media,
* sends a request to the server twice, and running a callback twice.
*
* This script fights the double callback issue, by wrapping a callback in function "Once".
*/
const appendFontAsync = (link, media , callback) => {
document.body.appendChild(link);
const img = document.createElement("img");
img.src = link.href;
img.onerror = () => isUndefined(callback) || once(callback);
setTimeout(() => link.media = media);
};
/*
* Font loader function
*/
const fontLoader = (prefix, cls) => {
const link = window.document.createElement("link"),
href = `${baseLocation}${parseFontsManifest(prefix + fontTypeSupported())}`;
linkExists(href)
||
(
link.rel = "preload",
link.href = href,
link.media = "all",
appendFontAsync(link, media(window.innerWidth),() => link.rel = "stylesheet"),
window.document.body.className += ` ${cls}`
);
};
/*
* Array-like object with all stylesheets appended to the DOM.
*/
const sheets = window.document.styleSheets;
/*
* Function to detect, if <body> tag has been rendered,
* and if it has been renderen, running a callback.
*/
const ready = cb => window.document.body ? cb() : setTimeout(() => ready( cb ));
/*
* This is a callback function for loadCSS modifyed callback.
*/
const firstRun = (firstrun, pageName) => {
firstrun
&& (
console.log("loadCSS callback fired"),
/*
* applyCss function on window resize.
*/
window.onresize = debounce(() => applyCss(pageName), 500)
//, You should put here all your stuff, which you want
// to be executed, after your CSS has been loaded.
//
// I personally use it to close the loading screen with spinner.
);
};
/*
* https://github.com/filamentgroup/loadCSS based function,
* with enhanced callback, and of course rewritten to ES6.
*
* Chrome has a bug, which in case of changing link.media,
* sends a request to the server twice, and running a callback twice.
*
* This script fights the double callback issue, by wrapping a callback in function "Once".
*/
const loadCSS = ( href, before, media, callback ) => {
console.log(`loadCSS fired!
href: ${href} media: ${media} callback: ${typeof callback}
`);
if (linkExists(href)) return;
const ss = window.document.createElement( "link" );
let ref, refs;
before
? (ref = before)
: (
refs = ( window.document.body || window.document.getElementsByTagName( "head" )[ 0 ] ).childNodes,
ref = refs[ refs.length - 1]
);
ss.rel = "stylesheet";
ss.href = href;
ss.media = "only x";
ready(() =>
ref.parentNode.insertBefore(ss, (before ? ref : ref.nextSibling))
);
function onloadcssdefined (cb) {
if (Object.keys(sheets).filter(sheet => sheets[sheet].href === ss.href).length) return cb();
setTimeout(() => onloadcssdefined( cb ));
}
function loadCB () {
isUndefined(callback) || once(callback);
ss.addEventListener
&& ss.removeEventListener( "load", loadCB );
ss.media = media || "all";
}
if (ss.addEventListener) ss.addEventListener( "load", loadCB);
ss.onloadcssdefined = onloadcssdefined;
onloadcssdefined(loadCB);
return ss;
};
/*
* applyCss function takes 2 arguments.
* type "string" : pageName
* type bool : firstrun
*/
const applyCss = (pageName, firstrun) => {
const width = window.innerWidth,
paths = responsivePaths(pageName);
loadCSS(
`${baseLocation}${parseCssManifest((width <= 767) ? paths.mobile : (width > 767 && width < 1200) ? paths.tablet : paths.desktop)}`,
void 0,
media(width),
firstRun(firstrun, pageName)
);
};
/*
* Export two functions, to use it as you want.
*
* In your project, you can import it:
*
* import { applyCss, fontLoader } from "./path/to/this/file.js";
*/
export { applyCss, fontLoader };
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment