DRY lazy-loaded images with <noscript> fallback - javascript
I know that (a handful of) non-JavaScript users are out there and I'd like to cater for them instead of giving them poorer experience just because of their preference (be that for privacy reasons or whatever).
Most lazy-loading JS libraries seem to address this in the same fashion, for example see lazysizes:
<style>
.no-js img.lazyload {
display: none;
}
</style>
<noscript>
<img src="image.jpg" />
</noscript>
<img src="grey.jpg" data-src="image.jpg" class="lazyload" />
Mainly out of curiosity, I got to wondering if it would be possible to pull the fallback out of the <noscript> tag and add it to the DOM programmatically with JavaScript so that the image source didn't have to be duplicated in two image tags which would leave me with just:
<noscript>
<img src="image.jpg" class="lazyload" width="600" height="400"/>
</noscript>
Here's what I've knocked together:
(function(attribute) {
Array.prototype.forEach.call(document.getElementsByTagName("noscript"), function(node) {
var parser = new DOMParser,
el = parser.parseFromString(node.textContent, "text/xml").documentElement, // XML => <img/> required
img = ("img" == el.tagName) ? el : el.getElementsByTagName("img")[0]; // allow for <img/> in <picture>
img.setAttribute(attribute, img.getAttribute("src"));
img.setAttribute("src", "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsQAAA7EAZUrDhsAAAANSURBVBhXYzh8+PB/AAffA0nNPuCLAAAAAElFTkSuQmCC");
node.insertAdjacentHTML("beforebegin", el.outerHTML);
});
})("data-src"); // different libraries use different data attribute names
This appears to work everywhere (Chrome, Safari, Opera, Firefox) except Internet Explorer (naturally). I know that .textContent isn't available pre-IE9 but IE9+ all seem to be failing at the final hurdle - the .outerHTML. Am I doomed to failure and having to repeat myself in my markup?
Update: To clarify, I'd ideally like to be able to use arbitrary attributes (alt, title, etc.) in the image tag or even use responsive markup:
<noscript>
<picture>
<source ... />
<source ... />
<img src="image.jpg" />
</picture>
</noscript>
I'm the creator of lazySizes. This approach has multiple porblems:
A noscript element is never renderend, which means it is not detectable, wether it is visible or not (or better said it is always invisible)
You can't use statefull classes lazyloading and lazyload to give feedback to the user
You can't pre-occupy the space for your lazy embed content (which is important for both a) user experience (no content jumping) and b) performance (no reflow)
(It has problems in older browsers)
The data-sizes="auto" feature can't be used
However if 4. and 5. isn't a problem for you, it is possible to use a noscript child element in conjunction with a lazyload parent to achieve this.
The markup could look something like this:
<div class="lazyload" data-noscript="">
<noscript>
<p>any kind of content you want to be unveiled</p>
</noscript>
</div>
And the lazySizes plugin code would look something like this:
(function(){
'use strict';
var supportPicture = !!window.HTMLPictureElement;
addEventListener('lazybeforeunveil', function(e){
if(e.defaultPrevented || e.target.getAttribute('data-noscript') == null){return;}
var imgs, i;
var noScript = e.target.getElementsByTagName('noscript')[0] || {};
var content = noScript.textContent || noScript.innerText || '';
e.target.innerHTML = content;
if(supportPicture){return;}
imgs = e.target.querySelectorAll('img[srcset], picture > img');
for(i = 0; i < imgs.length; i++){
lazySizes.uP(imgs[i]);
}
});
})();
In case you like this, I might make an official plugin for this. Here is the plugin: https://github.com/aFarkas/lazysizes/tree/master/plugins/noscript
Here's how I'd do it, using methods that should be available in all browsers
(function(attribute) {
Array.prototype.forEach.call(document.getElementsByTagName("noscript"), function(node) {
var content = node.childNodes[0].nodeValue,
parser = new DOMParser(),
doc = parser.parseFromString(content, "text/html"),
images = doc.getElementsByTagName('img');
for (var i=images.length; i--;) {
var img = document.createElement('img');
img.src = 'data:image/png;base64,iVBOR ....';
img.height = images[i].getAttribute('height');
img.width = images[i].getAttribute('width');
img.setAttribute(attribute, images[i].getAttribute('src'));
node.parentNode.insertBefore(img, node.nextSibling);
}
});
})("data-src");
Here's the trick I use:
(function() {
"use strict";
var config = {
// If the image gets within 50px in the Y axis, start the download.
rootMargin: "50px 0px",
threshold: 0.01
};
var observer;
//If we're using a browser without the IntersectionObserver (IE11, Safari 11), skip the lazy part and just load the resources
if ("IntersectionObserver" in window) {observer = new IntersectionObserver(onIntersection, config);}
//If we're using a browser without requestAnimationFrame (IE9, Opera Mini), just run the passed function
var rAF;
if ("requestAnimationFrame" in window) rAF = window.requestAnimationFrame;
else rAF = function(func) { func(); };
var tempImg = "data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==";
/**
* Temporarily replace a expensive resource load with a cheap one
*/
function storeSourceForLater(lazyItem, tempData) {
//Store our ACTUAL source for later
lazyItem.setAttribute("data-lazy-src", lazyItem.getAttribute("src"));
//Set the item to point to a temporary replacement (like a data URI)
lazyItem.setAttribute("src", tempData);
//Now observe the item so that we can start loading when it gets close to the viewport
observer.observe(lazyItem);
}
/**
* Temporarily prevent expensive resource loading by inserting a <source> tag pointing to a cheap one (like a data URI)
*/
function jamSourceLoading(lazyItem, tempData) {
var newSource = document.createElement("source");
newSource.setAttribute("srcset", tempData);
newSource.setAttribute("data-lazy-remove", "true");
//adding this source tag at the start of the picture tag means the browser will load it first
lazyItem.insertBefore(newSource, lazyItem.firstChild);
var baseImage = lazyItem.getElementsByTagName("img")[0];
if (baseImage) {
//this is a picture tag, so we need to watch the image (as the picture tag is smaller than the image usually)
observer.observe(baseImage);
}
}
/**
* Set up the lazy items so that they won't try to load when we add them to the document, but will once the user is close to seeing them
*/
function prepareLazyContents(lazyArea) {
var lazyImgs = lazyArea.getElementsByTagName("img");
for(var i = lazyImgs.length; i--;){
storeSourceForLater(lazyImgs[i], tempImg);
}
var lazyPictures = lazyArea.getElementsByTagName("picture");
for(var i3 = lazyPictures.length; i3--;) {
jamSourceLoading(lazyPictures[i3], tempImg);
}
}
/**
* Put the source back where we found it - now that the element is attached to the document, it will load now
*/
function restoreSource(lazyItem) {
lazyItem.setAttribute("src", lazyItem.getAttribute("data-lazy-src"));
lazyItem.removeAttribute("data-lazy-src");
}
/**
* Remove the source tag preventing the loading of picture/audio/video
*/
function removeJammingSource(lazyItem) {
var jammingSource = lazyItem.querySelector("source[data-lazy-remove]");
if (jammingSource) lazyItem.removeChild(jammingSource);
}
/**
* Handle the intersection postback
*/
function onIntersection(entries, obsvr) {
entries.forEach(function(entry) {
if(entry.intersectionRatio === 0) return;
//if the item is now visible, load it and stop watching it
var lazyItem = entry.target;
obsvr.unobserve(lazyItem);
//Just in case the img is the decendent of a picture element, check for source tags
removeJammingSource(lazyItem.parentNode);
restoreSource(lazyItem);
});
}
/**
* Retrieve the elements from the 'lazy load' no script tags and prepare them for display
*/
function setUp() {
//Get all the noscript tags on the page
var lazyLoadAreas = document.getElementsByTagName("noscript");
for(var i = lazyLoadAreas.length; i--;) {
var noScriptTag = lazyLoadAreas[i];
//only process the ones marked for lazy loading
if (!noScriptTag.hasAttribute("data-lazy-load")) continue;
// The contents of a noscript tag are treated as text to JavaScript
var lazyAreaHtml = noScriptTag.textContent||noScriptTag.innerHTML;
// So we stick them in the innerHTML of a new div tag to 'load' them
var lazyArea = document.createElement("div");
lazyArea.innerHTML = lazyAreaHtml;
//Only delay loading if we can use the IntersectionObserver to check for visibility
if(!observer) {
noScriptTag.parentNode.replaceChild(lazyArea, noScriptTag);
} else {
prepareLazyContents(lazyArea);
noScriptTag.parentNode.replaceChild(lazyArea, noScriptTag);
}
}
}
//If the page has loaded already, run setup - if it hasn't, run as soon as it has.
//Use requestAnimationFrame as this will propably cause repaints
if (/comp|inter/.test(document.readyState)) {
rAF(setUp);
} else if ("addEventListener" in document) {
document.addEventListener("DOMContentLoaded",
function(){rAF(setUp);});
} else {
document.attachEvent("onreadystatechange", function() {
if (document.readyState=="complete") {
setUp();
}
});
}
})();
<p>Scroll down to see lazy loading in action!</p>
<noscript><p>Even with JavaScript turned off, the images should still load.</p></noscript>
<p>Why are the assets in noscript tags? So that they will load for people who have turned JavaScript off!</p>
<p>(The conditional comments are becuase there is no way to fetch the contents of a noscript tag in IE8 and below.)</p>
<hr/>
<div style="height: 600px;"></div>
<hr/>
<!--[if (gt IE 8)|!(IE)]><!--><noscript data-lazy-load><!--<![endif]-->
<img src="//upload.wikimedia.org/wikipedia/commons/2/27/F-16_Plan_Black_on_Circle_Light_Blue.svg?c=25" alt="This is an image used to demonstrate a lazy-loading trick." width="250" height="250">
Here is some text on the outside to demonstrate the lack of reflows!
<!--[if (gt IE 8)|!(IE)]><!--></noscript><!--<![endif]-->
<hr/>
<!--[if (gt IE 8)|!(IE)]><!--><noscript data-lazy-load><!--<![endif]-->
<picture>
<img src="//upload.wikimedia.org/wikipedia/commons/2/27/F-16_Plan_Black_on_Circle_Light_Blue.svg?c=25" alt="This is an image used to demonstrate a lazy-loading trick." width="250" height="250">
</picture>
This one is a reponsive picture element!
<!--[if (gt IE 8)|!(IE)]><!--></noscript><!--<![endif]-->
It only Lazy-Loads on browser that support Intersection Observer (so not IE, but about 87% of the world at time of writing) but the image will show in all browsers.
Since 2019 the img tag has a new attribute: loading. You can specify loading="lazy" which defers loading the image until it reaches a calculated distance from the viewport, as defined by the browser.
It has broad browser support (Edge, Firefox, Chrome, Safari and Opera):
It defaults to eager/normal loading in case of older browsers or when Javascript is disabled. The latter is an anti-tracking measure, because if a user agent supported lazy loading when scripting is disabled, it would still be possible for a site to track a user's approximate scroll position by strategically placing images in a page's markup. (source: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/img#attr-loading)
Related
Reloading stylesheet is fluid in Chrome, but weird in Firefox (jQuery)
I am using the following code to reload a stylesheet when the user makes a selection: <link type="text/css" id="main_style" href="css/style.php" rel="stylesheet"> <button id="secret_1" style="display:none;"></button> $(document).ready(function(){ function freshStyle(stylesheet){ $('#main_style').attr('href',stylesheet); } $('#secret_1').click(function(event){ event.preventDefault(); var restyled = 'style.php?v='+Math.floor(Math.random() * 10000); freshStyle(restyled); }); }); In Chrome, the reload happens fluidly, and the transitions look great. In Firefox, the website temporarily becomes a garbled mess (while the stylesheet is being reloaded) for a second before the new stylesheet is active. Is this something that can be solved with code, or is this just a feature of the Firefox browser?
If you load the new stylesheet and remove the old one once the new takes effect, the flash of unstyled format should no longer happen Note: I've done away with jquery inside the .ready since I don't really know how to do a lot of what is happening here in jQuery - vanilla JS all the way for me (but you can convert to jquery if you're more comfortable with it) $(document).ready(function() { function freshStyle(stylesheet) { const id = 'main_style'; const main = document.getElementById(id); const sibling = main.nextElementSibling; const parent = main.parentElement; const style = document.createElement('link'); style.rel = 'stylesheet'; style.href = stylesheet; style.onload = () => { // load fires BEFORE style is applied - so delay a tick setTimeout(() => { // remove the old stylesheet main.remove(); // set the id of the new sheet to the removed one style.id = id; }, 0); }; // this just ensures the new stylesheet ends up exactly where the old was parent.insertBefore(style, sibling); } document.getElementById('secret_1').addEventListener('click', (e) => { e.preventDefault(); const restyled = `style.php?v=${Math.floor(Math.random() * 10000)}`; freshStyle(restyled); }); });
Stylesheet for everything
I want to load the CSS file for everything. For elements: tabs, sidebars, tags , windows, options, developer tools, etc. I need this to change the scroll bars. How to do it in Firefox addons-sdk?
By evrything you actually mean the browser area. So what you want to do is write CSS within the brackets of #-moz-document url("chrome://browser/content/browser.xul") { and }. Otherwise the CSS will affect things within webpages. There are two ways to load in a CSS sheet, one is with the nsIStyleSheetService and one is with window.loadSheet. The window.loadSheet is the recommended way. You would do it something like this: function registerWindows() { var _uri = Services.io.newURI("chrome://aus-view/skin/toolbar.css", null, null); aWindow.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils).loadSheet(_uri, 1); } function unregisterWindows() { var _uri = Services.io.newURI("chrome://aus-view/skin/toolbar.css", null, null); aWindow.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils).removeSheet(_uri, 1); } You would have to make sure to load your sheet into newly opened windows. With the nsIStyleSheetService, you just loadAndRegisterSheet and then you don't have to worry about window opening and closing. But it's harder on performance I heard. I don't know the source on this performance though. Cu.import('resource://gre/modules/Services.jsm'); var sss = Cc['#mozilla.org/content/style-sheet-service;1'].getService(Ci.nsIStyleSheetService); var cssUri = Services.io.newURI('chrome://content/path/to/your/file.css', null, null); sss.loadAndRegisterSheet(cssUri, sss.USER_SHEET); Then when you want to remove it you just do: sss.unregisterSheet(cssUri, sss.USER_SHEET); Now those both used files. You can make a URI without any files like this: var css = ''; css += '* { background-color: red; }'; css += '*.hover { background-color: blue; }'; var cssEncoded = encodeURIComponent(css); var cssEncodedWithDataURL = 'data:text/css,' + cssEncoded Then we just make our URI the same way: var cssUri = Services.io.newURI(cssEncodedWithDataURL, null, null); Then you just load the stylehseet the same way. (Example using 2nd method: sss.unregisterSheet(cssUri, sss.USER_SHEET))
Refresh iframe content without white flash
I have an iframe in my site that must reload every second (it's just reloading simple HTML for data analysis). This causes a really annoying white flash each time it reloads. I have put hours of research into trying to remove this flash. Here's what I have (which doesn't work), which creates a new iframe, adds it to the DOM, and then removes the old one, effectively reloading the page. It still flashes white though: $( document ).ready(function() { setInterval(function() { var container = document.getElementById('bottom-frame-container'); var source = container.getElementsByTagName('iframe')[0]; var newFrame = document.createElement('iframe'); for (i = 0; i < source.attributes.length; i++) { var attr = source.attributes[i]; newFrame.setAttribute(attr.name, attr.value); } newFrame.style.visibility = 'hidden'; newFrame.id = 'bottom-frame'; container.appendChild(newFrame); newFrame.style.visibility = 'visible'; container.removeChild(container.getElementsByTagName('iframe')[0]); }, 1000); }); The iframe code is simply: <div id="bottom-frame-container"> <iframe id="bottom-frame" width="100%" height="60%" frameborder="0" allowTransparency="true" src="http://10.62.0.15/haproxy?stats"> </div> I'm completely open to any suggestions or alternative approaches here, I'm getting a bit desperate!
Here's how i would handle this issue : have one 'load' div on top of the iframe to prevent the blinking from being seen. Then before loading you fadein the warning --> when fadein is finished you trigger load --> when load is finished you fadeout the warning. fiddle is here : http://jsfiddle.net/bZgFL/2/ html is : <iframe width=500px height=300px id = 'myFrame' style='position:absolute;top:0;' src = 'http://jsfiddle.net/' > </iframe> <div id = 'myLoad' style='float:top;top:0; position:absolute; background-color:#CCC; min-width:500px; min-height:300px; '> <div style='position:absolute;top:130px;left:200px;'> <b> Loading... </b> </div> </div> </div> code is (using jQuery) var ifr = document.getElementById('myFrame'); setFrame(); setInterval(setFrame, 3000); function setFrame() { $('#myLoad').fadeIn(200, setAdress); } function setAdress() { ifr.onload=function() { $('#myLoad').fadeOut(200) ;} ifr.src = 'http://jsfiddle.net/'; } Obviously it needs fine tuning for the look, and if you want the user to be able to click the iframe, you'll have to disable pointer on top div, but it should get you going.
Can you put the two iFrames on the page and hide show them with CSS. This much less of an impact on the page than removing and inserting elements into the DOM. Then add an onload event to both of them that triggers the current iFrame to hide and reload, while the new on shows? Use RequestAninationFrame to help reduce flicker. Code would look something like this. var currentIFrame = 1; $('iframe').on('load',function(){ $(this).show() $('#iframe'+(currentIFrame=1-currentIFrame)).hide().delay(1000).reload(); }) This way you only do the switch once your sure the content has fully loaded.
I know this is an old question but I'm really not sure why someone didn't post this obvious solution.... Just paste this javascript code into any page that has an iframe. <script> // Preventing whiteflash in loading iframes. (function () { var div = document.createElement('div'), ref = document.getElementsByTagName('base')[0] || document.getElementsByTagName('script')[0]; div.innerHTML = '<style> iframe { visibility: hidden; } </style>'; ref.parentNode.insertBefore(div, ref); window.onload = function() { div.parentNode.removeChild(div); } })(); </script> It will simply renfer any iframe visibility:hidden until which time the iframe is loaded and then make it visible.
Busting a tough FRAME killer
I've been trying to break this up for a few hours now but with no success... I am pretty desperate now :( I am doing penetration testing for a company and I need to bypass this frame killer JS: <script type="text/javascript">/* <![CDATA[ */ if (top != self) { try { if (parent != top) { throw 1; } var disallowed = ['XXXXXXX.com']; var href = top.location.href.toLowerCase(); for (var i = 0; i < disallowed.length; i++) { if (href.indexOf(disallowed[i]) >= 0) { throw 1; } } } catch (e) { try { window.document.getElementsByTagName('head')[0].innerHTML = ''; } catch (e) { /* IE */ var htmlEl = document.getElementsByTagName('html')[0]; htmlEl.removeChild(document.getElementsByTagName('head')[0]); var el = document.createElement('head'); htmlEl.appendChild(el); } window.document.body.innerHTML = '<img src="http://www.XXXXXXX.com/img/XXXXXX.gif" style="border:0px;" /><br />Go to XXXXXXX.com'; } } /* ]]> */</script> Thank you very much!
Use one of the following: IP Address instead of domain name to bypass the disallowed list X-Frame-Options HTML5 sandbox If the body element's node document's browsing context is a nested browsing context, and the browsing context container of that nested browsing context is a frame or iframe element, then the container frame element of the body element is that frame or iframe element. Otherwise, there is no container frame element. The above requirements imply that a page can change the margins of another page (including one from another origin) using, for example, an iframe. This is potentially a security risk, as it might in some cases allow an attack to contrive a situation in which a page is rendered not as the author intended, possibly for the purposes of phishing or otherwise misleading the user. References HTML5 Specification, Section 10: Rendering
link element onload
Is there anyway to listen to the onload event for a <link> element? F.ex: var link = document.createElement('link'); link.rel = 'stylesheet'; link.href = 'styles.css'; link.onload = link.onreadystatechange = function(e) { console.log(e); }; This works for <script> elements, but not <link>. Is there another way? I just need to know when the styles in the external stylesheet has applied to the DOM. Update: Would it be an idea to inject a hidden <iframe>, add the <link> to the head and listen for the window.onload event in the iframe? It should trigger when the css is loaded, but it might not guarantee that it's loaded in the top window...
Today, all modern browsers support the onload event on link tags. So I would guard hacks, such as creating an img element and setting the onerror: if !('onload' in document.createElement('link')) { imgTag = document.createElement(img); imgTag.onerror = function() {}; imgTag.src = ...; } This should provide a workaround for FF-8 and earlier and old Safari & Chrome versions. minor update: As Michael pointed out, there are some browser exceptions for which we always want to apply the hack. In Coffeescript: isSafari5: -> !!navigator.userAgent.match(' Safari/') && !navigator.userAgent.match(' Chrom') && !!navigator.userAgent.match(' Version/5.') # Webkit: 535.23 and above supports onload on link tags. isWebkitNoOnloadSupport: -> [supportedMajor, supportedMinor] = [535, 23] if (match = navigator.userAgent.match(/\ AppleWebKit\/(\d+)\.(\d+)/)) match.shift() [major, minor] = [+match[0], +match[1]] major < supportedMajor || major == supportedMajor && minor < supportedMinor
This is kind of a hack, but if you can edit the CSS, you could add a special style (with no visible effect) that you can listen for using the technique in this post: http://www.west-wind.com/weblog/posts/478985.aspx You would need an element in the page that has a class or an id that the CSS will affect. When your code detects that its style has changed, the CSS has been loaded. A hack, as I said :)
The way I did it on Chrome (not tested on other browsers) is to load the CSS using an Image object and catching its onerror event. The thing is that browser does not know is this resource an image or not, so it will try fetching it anyway. However, since it is not an actual image it will trigger onerror handlers. var css = new Image(); css.onerror = function() { // method body } // Set the url of the CSS. In link case, link.href // This will make the browser try to fetch the resource. css.src = url_of_the_css; Note that if the resource has already been fetched, this fetch request will hit the cache.
E.g. Android browser doesn't support "onload" / "onreadystatechange" events for element: http://pieisgood.org/test/script-link-events/ But it returns: "onload" in link === true So, my solution is to detect Android browser from userAgent and then wait for some special css rule in your stylesheet (e.g., reset for "body" margins). If it's not Android browser and it supports "onload" event- we will use it: var userAgent = navigator.userAgent, iChromeBrowser = /CriOS|Chrome/.test(userAgent), isAndroidBrowser = /Mozilla\/5.0/.test(userAgent) && /Android/.test(userAgent) && /AppleWebKit/.test(userAgent) && !iChromeBrowser; addCssLink('PATH/NAME.css', function(){ console.log('css is loaded'); }); function addCssLink(href, onload) { var css = document.createElement("link"); css.setAttribute("rel", "stylesheet"); css.setAttribute("type", "text/css"); css.setAttribute("href", href); document.head.appendChild(css); if (onload) { if (isAndroidBrowser || !("onload" in css)) { waitForCss({ success: onload }); } else { css.onload = onload; } } } // We will check for css reset for "body" element- if success-> than css is loaded function waitForCss(params) { var maxWaitTime = 1000, stepTime = 50, alreadyWaitedTime = 0; function nextStep() { var startTime = +new Date(), endTime; setTimeout(function () { endTime = +new Date(); alreadyWaitedTime += (endTime - startTime); if (alreadyWaitedTime >= maxWaitTime) { params.fail && params.fail(); } else { // check for style- if no- revoke timer if (window.getComputedStyle(document.body).marginTop === '0px') { params.success(); } else { nextStep(); } } }, stepTime); } nextStep(); } Demo: http://codepen.io/malyw/pen/AuCtH
Since you didn't like my hack :) I looked around for some other way and found one by brothercake. Basically, what is suggested is to get the CSS using AJAX to make the browser cache it and then treat the link load as instantaneous, since the CSS is cached. This will probably not work every single time (since some browsers may have cache turned off, for example), but almost always.
Another way to do this is to check how many style sheets are loaded. For instance: With "css_filename" the url or filename of the css file, and "callback" a callback function when the css is loaded: var style_sheets_count=document.styleSheets.length; var css = document.createElement('link'); css.setAttribute('rel', 'stylesheet'); css.setAttribute('type', 'text/css'); css.setAttribute('href', css_filename); document.getElementsByTagName('head').item(0).appendChild(css); include_javascript_wait_for_css(style_sheets_count, callback, new Date().getTime()); function include_javascript_wait_for_css(style_sheets_count, callback, starttime) /* Wait some time for a style sheet to load. If the time expires or we succeed * in loading it, call a callback function. * Enter: style_sheet_count: the original number of style sheets in the * document. If this changes, we think we finished * loading the style sheet. * callback: a function to call when we finish loading. * starttime: epoch when we started. Used for a timeout. 12/7/11-DWM */ { var timeout = 10000; // 10 seconds if (document.styleSheets.length!=style_sheets_count || (new Date().getTime())-starttime>timeout) callback(); else window.setTimeout(function(){include_javascript_wait_for_css(style_sheets_count, callback, starttime)}, 50); }
This trick is borrowed from the xLazyLoader jQuery plugin: var count = 0; (function(){ try { link.sheet.cssRules; } catch (e) { if(count++ < 100) cssTimeout = setTimeout(arguments.callee, 20); else console.log('load failed (FF)'); return; }; if(link.sheet.cssRules && link.sheet.cssRules.length == 0) // fail in chrome? console.log('load failed (Webkit)'); else console.log('loaded'); })(); Tested and working locally in FF (3.6.3) and Chrome (linux - 6.0.408.1 dev) Demo here (note that this won't work for cross-site css loading, as is done in the demo, under FF)
You either need a specific element which style you know, or if you control the CSS file, you can insert a dummy element for this purpose. This code will exactly make your callback run when the css file's content is applied to the DOM. // dummy element in the html <div id="cssloaded"></div> // dummy element in the css #cssloaded { height:1px; } // event handler function function cssOnload(id, callback) { setTimeout(function listener(){ var el = document.getElementById(id), comp = el.currentStyle || getComputedStyle(el, null); if ( comp.height === "1px" ) callback(); else setTimeout(listener, 50); }, 50) } // attach an onload handler cssOnload("cssloaded", function(){ alert("ok"); }); If you use this code in the bottom of the document, you can move the el and comp variables outside of the timer in order to get the element once. But if you want to attach the handler somewhere up in the document (like the head), you should leave the code as is. Note: tested on FF 3+, IE 5.5+, Chrome
The xLazyLoader plugin fails since the cssRules properties are hidden for stylesheets that belong to other domains (breaks the same origin policy). So what you have to do is compare the ownerNode and owningElements. Here is a thorough explanation of what todo: http://yearofmoo.com/2011/03/cross-browser-stylesheet-preloading/
// this work in IE 10, 11 and Safari/Chrome/Firefox/Edge // if you want to use Promise in an non-es6 browser, add an ES6 poly-fill (or rewrite to use a callback) let fetchStyle = function(url) { return new Promise((resolve, reject) => { let link = document.createElement('link'); link.type = 'text/css'; link.rel = 'stylesheet'; link.onload = resolve; link.href = url; let headScript = document.querySelector('script'); headScript.parentNode.insertBefore(link, headScript); }); };
This is a cross-browser solution // Your css loader var d = document, css = d.head.appendChild(d.createElement('link')) css.rel = 'stylesheet'; css.type = 'text/css'; css.href = "https://unpkg.com/tachyons#4.10.0/css/tachyons.css" // Add this if (typeof s.onload != 'undefined') s.onload = myFun; } else { var img = d.createElement("img"); img.onerror = function() { myFun(); d.body.removeChild(img); } d.body.appendChild(img); img.src = src; } function myFun() { /* ..... PUT YOUR CODE HERE ..... */ } The answer is based on this link that say: What happens behind the scenes is that the browser tries to load the CSS in the img element and, because a stylesheet is not a type of image, the img element throws the onerror event and executes our function. Thankfully, browsers load the entire CSS file before determining its not an image and firing the onerror event. In modern browsers you can do css.onload and add that code as a fallback to cover old browsers back to 2011 when only Opera and Internet Explorer supported the onload event and onreadystatechange respectively. Note: I have answered here too and it is my duplicate and deserves to be punished for my honesteness :P