Skip to content

Instantly share code, notes, and snippets.

@sterlingrobot
Last active April 5, 2019 14:37
Show Gist options
  • Save sterlingrobot/cba14504c8a3d510b7d397cd8a0464bd to your computer and use it in GitHub Desktop.
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
/*! 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>&lt;link data-wscss&gt;</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