Skip to content

Instantly share code, notes, and snippets.

@JustFly1984
Last active April 11, 2016 14:02
Show Gist options
  • Save JustFly1984/8420b137721b76be8f304b3ab43f2dec to your computer and use it in GitHub Desktop.
Save JustFly1984/8420b137721b76be8f304b3ab43f2dec 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 isFunction = o => {}.toString.apply(o) === "[object Function]",
/*
* 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 returns wrapped function,
* and swaps it with empty function body.
*
* If you want to pass arguments to wrapped function,
* you need to pass them to "Once" function as 3rd, 4th,...rest arguments.
*/
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".
*
* Link loaded to the head, before any style tags, to be inherited by css in inline style tag.
*
* media
*/
const appendFontAsync = (link, media, callback ) => {
Array.from(document.getElementsByTagName("head"))[0].insertBefore(link, Array.from(document.getElementsByTagName("style"))[0]);
const img = document.createElement("img");
img.src = link.href;
img.onerror = () => (
/*
* If callback is Function, we are wrapping callback in "Once" function,\
* and executing wrapped callback.
*/
isFunction(callback) && once(callback)()
);
};
/*
* 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 () {
/*
* Here we are not executing callback, but passing wrapped in "Once" function
* to addEventListener, so it will be executed then link will be loaded.
*/
isFunction(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 };
@JustFly1984
Copy link
Author

I have been tracing some bugs in my code, so now it is updated version.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment