-
-
Save abodacs/b41f53102bda2c697bd1e28e5d9cab0b to your computer and use it in GitHub Desktop.
Async CSS and Fonts DOM injection ES6 library
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
* 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