-
-
Save sterlingrobot/cba14504c8a3d510b7d397cd8a0464bd to your computer and use it in GitHub Desktop.
Check that a non-blocking stylesheet has been loaded and parsed before executing Javascript
This file contains 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
/*! GENERIC-STYLESLOADED */ | |
/** | |
* Utility function to wait until non-blocking CSS assets have | |
* loaded before executing a callback. | |
* | |
* @info <p>Typically, since assets are loaded from same domain, | |
* this function can verify if there are CSS rules associated | |
* with a given stylesheet. However, there are some cases | |
* (eg. staging, betamode, etc) where trying to access CSS rules | |
* throws an InvalidAccessError. In these cases, a loadevent listener | |
* is attached to set <code>data-wscss="loaded"</code>. | |
* Failing that, the Deferred is set to resolve after a given timeout, | |
* to avoid endless polling.</p> | |
* <p><strong>NOTE:</strong> You shouldn't need to use this directly. | |
* This function is imported and used by OCOM framework to determine | |
* if it should wait to run component JS when async CSS is found | |
* in the <code>head</code>. Therefore, inside any component code registered | |
* with <code>OCOM.register</code> it is <u>safe to assume styles have | |
* already been applied</u>.</p> | |
* | |
* @class OCOM.stylesLoaded | |
* @see [https://api.jquery.com/jQuery.Deferred/](jQuery.Deferred) | |
* @returns {jQuery.Deferred} a jQuery Deferred object (Promise) which | |
* resolves once <strong>all</strong> | |
* <code><link data-wscss></code> assets have loaded | |
* @example <caption>Anonymous callback</caption> | |
* OCOM.stylesLoaded().then(function() { | |
* // do something now that styles have been applied ... | |
* }); | |
* @example <caption>Callback reference</caption> | |
* OCOM.stylesLoaded().then(callbackFn); | |
*/ | |
(function($, window) { | |
/* eslint-disable no-console */ | |
'use strict'; | |
// iOS 8.1-8.4 does not support window.performance.now, fallback to Date.now() | |
var start = window.performance && window.performance.now() || Date.now(), | |
timeout = 10000, | |
cached = false, | |
promise = $.Deferred(), | |
DEBUG = 'OCOM' in window && OCOM.debug, | |
ATTR_WSCSS = 'data-wscss', | |
DATA_LOADED = 'loaded', | |
DATA_EVLISTEN = 'loadEventListener', | |
inDocSheets = function(link) { | |
// checks if link.href has been applied to document | |
return ( | |
Array.prototype.slice.call(document.styleSheets) | |
.map(function(sheet) { | |
return sheet.href; | |
}) | |
.some(function(href) { | |
return href === link.href; | |
}) | |
); | |
}, | |
getSheetName = function(sheet) { | |
return sheet.href && sheet.href.slice(sheet.href.lastIndexOf('/') + 1); | |
}, | |
check = function(link) { | |
var loaded = false, | |
foundRules = 0, | |
now = window.performance && window.performance.now() || Date.now(), | |
isCrossDomain = !(new RegExp(window.location.origin).test(link.href)), | |
isBlocking = !link.as && link.rel !== 'preload', | |
isPreLoad = ( | |
link.as === 'style' && | |
link.rel === 'stylesheet' && | |
link.getAttribute(ATTR_WSCSS) !== 'error' | |
), | |
loadFired = link.getAttribute(ATTR_WSCSS) === DATA_LOADED, | |
timeoutFallback = (now - start > timeout), | |
log = '', | |
// cache a jQuery instance for event listener and data | |
$link = $(link); | |
if(DEBUG) { | |
log += '[' + parseInt(now, 10) + 'ms] ' + getSheetName(link); | |
} | |
try { | |
// check if rules have been parsed and applied to document | |
// In some browsers, if a stylesheet is loaded from a different domain, | |
// calling cssRules results in SecurityError | |
// https://developer.mozilla.org/en-US/docs/Web/API/CSSStyleSheet#Notes | |
foundRules = link.sheet.cssRules.length; | |
loaded = Boolean(foundRules) && inDocSheets(link); | |
} catch(err) { | |
// attach a listener for load | |
if(!$link.data(DATA_EVLISTEN)) { | |
// set attr value on load | |
// set flag for listener has already been attached | |
$link.one('error', $link.attr.bind($link, ATTR_WSCSS, 'error')) | |
.one('load', $link.attr.bind($link, ATTR_WSCSS, DATA_LOADED)) | |
.data(DATA_EVLISTEN, true); | |
} | |
loaded = | |
// CSSStyleSheet object exists in CSSOM | |
// but doesn't reliably mean external content has loaded | |
Boolean(link.sheet) && | |
// if cross-domain, we cannot access cssRules (eg. staging, betamode) | |
// so need to rely on preload handler, or our load event listener | |
isCrossDomain && | |
// if its a blocking CSS (*-base.css) ignore rest of checks | |
(isBlocking || | |
// catches preload support with onload="this.rel='stylesheet'" | |
// but can return false positive onerror="this.rel='stylesheet'" | |
isPreLoad || | |
// catches load event listener | |
loadFired) && | |
// make sure this sheet has been applied to document | |
inDocSheets(link); | |
} | |
if(DEBUG) { | |
log += [ | |
loaded || timeoutFallback ? ' ✓ready' : ' ✗not ready', | |
isCrossDomain ? ' ✗crossdomain' : ' ✓sameorigin', | |
(foundRules ? ' ✓' : ' ✗') | |
+ 'cssRules(' + (isCrossDomain ? 'N/A' : foundRules) + ')', | |
(inDocSheets(link) ? ' ✓' : ' ✗') + 'document.styleSheets', | |
isBlocking ? ' ✓blocking' | |
: isPreLoad ? ' ✓preloaded' | |
: loadFired ? ' ✓onload fired' | |
: timeoutFallback ? ' ✓timeout fallback' : '', | |
].join('\n'); | |
console.log(log); | |
} | |
// if things don't work due to CORS or 404, | |
// we should just go ahead after a reasonable timeout - 10sec | |
return loaded || timeoutFallback; | |
}, | |
stylesLoaded = function() { | |
// check for async css files | |
var sheets = $('link[' + ATTR_WSCSS + ']').get(); | |
// if check for stylesLoaded is true, cache for future (resize) calls | |
// if no async css are found, length = 0 and returns true | |
// otherwise, do check to look for CSS rules | |
cached = cached || | |
!sheets.length || | |
sheets.every(check); | |
// when our checks return true, we resolve the promise | |
// otherwise, recheck at an interval until we can resolve | |
return cached && promise.resolve() | |
|| setTimeout(stylesLoaded, 200) && promise; | |
}; | |
return stylesLoaded; | |
}(jQuery, window)); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment