2

I've got a custom slideshow page with hundreds of large pictures in markup. In addition, there is an adjustment to the layout that can't be done with CSS alone, and requires javascript. That adjustment depends on the computed CSS styles of a couple page elements.

Here's the issue: getting a computed CSS style on (document).ready doesn't seem to work. That makes perfect sense, because layout/paint haven't occurred yet when the DOM is merely registered.

(window).load is the obvious answer here. But the waiting period for (window).load is maddeningly long in this case, because it takes literally 30 seconds (longer on slow connections!) for all the 100+ large images to be downloaded. That's fine in principle for the slideshow, because the slideshow starts being usable once the first two images are loaded, long before all the images are loaded. The thing is, I'd like the scripted CSS layout correction to occur at that point as well.

To check for imgs being loaded, I'm use imageLoaded, which is great, and gets around the issues with jquery's load event when used with imgs. But how do I check for "load"-like events (and the availability of computed styles) on a page element, for example a div? (I realize divs don't fire load events.) In principle, is there a reliable cross-browser DOM event in between (document).ready and (window).load that I can listen for on a particular set of elements, to get computed styles? Or am I forced to choose between these two temporal extremes?

Ben
  • 11,082
  • 8
  • 33
  • 47
  • Would you be able to put off adding the slideshow images until after window.load? That would ensure that the rest of your page is done loading and useable before you attempt to load in hundreds of large images. – Kevin B Sep 26 '12 at 18:25
  • @Kevin That's definitely an idea, but it's something of a last resort, because it complicates things enormously. I was trying to keep everything in markup, because it's so simple to edit and maintain, and it degrades effortlessly when js is disabled. – Ben Sep 26 '12 at 18:41
  • 1
    It could be as simple as giving it a class with `display:none;` that gets removed by javascript, then using noscript tags to override it with `display:block;`. – Kevin B Sep 26 '12 at 18:43
  • @Kevin That's clever. I'm using modernizr "js" and "no-js" classes already, so that would be a cinch. But it hinges of course on browsers not downloading assets that are set to display:none. And last I checked, browsers still download assets set to display: none. So window.load wouldn't actually fire any sooner, would it? – Ben Sep 26 '12 at 19:03
  • I was under the impression that browsers did not load images that were hidden. That's why preloaders are needed for menus that have hover images that aren't using image sprites. – Kevin B Sep 26 '12 at 19:24
  • Well, background images in css are loaded conditionally based on the display property. But imgs in markup are always loaded:http://stackoverflow.com/questions/3315192/ – Ben Sep 26 '12 at 19:35

3 Answers3

3

On CSS load

Source: https://stackoverflow.com/a/12570580/1292652

Add a unique reference styles to the CSS files you want to check for like so:

#ensure-cssload-0 {
  display: none;
}

Then, use a JS function, cssLoad(), to repeatedly check whether the CSS has been downloaded (not sure if this is significantly different from when the CSS is actually painted/rendered):

var onCssLoad = function (options, callback) {
    var body = $("body");
    var div = document.createElement(constants.TAG_DIV);
    for (var key in options) {
        if (options.hasOwnProperty(key)) {
            if (key.toLowerCase() === "css") {
                continue;
            }
            div[key] = options[key];
        }
    }

    var css = options.css;
    if (css) {
        body.appendChild(div);
        var handle = -1;
        handle = window.setInterval(function () {
            var match = true;
            for (var key in css) {
                if (css.hasOwnProperty(key)) {
                    match = match && utils.getStyle(div, key) === css[key];
                }
            }

            if (match === true) {
                window.clearTimeout(handle);
                body.removeChild(div);
                callback();
            }
        }, 100);
    }
}

You can use it as:

onCssLoad({
    "id": <insert element CSS applies to>,
     css: <insert sample CSS styles>
}, function () {
    console.log("CSS loaded, you can show the slideshow now :)");
});


On CSS computation

Source: http://atomicrobotdesign.com/blog/javascript/get-the-style-property-of-an-element-using-javascript/

The previous solution only told if a CSS file had been 'downloaded', while in your question you mention you need to know when a style for an element has been 'computed' or 'rendered' or 'painted'. I hope this might resolve that.

Add a unique reference styles to the CSS files you want to check for like so:

#ensure-cssload-0 {
  ignored-property: 'computed';
}

Now, use getPropertyValue and getComputedStyle to check for the CSS:

function getStyle(elem, prop) {
    return window.getComputedStyle(elem, null).getPropertyValue(prop);
}

Now, just use a while loop with a callback:

function checkStyle(elem, prop, callback) {
    while ( getStyle(elem, prop) !== 'computed' ) {
        // see explanation below
    }
    callback()
}

Since JavaScript doesn't have a pass statement (like in Python), we use an empty code block.

If you want to retrieve all the CSS styles applied to an element (warning: this will also show inherited styles), check out this SO answer.


Avoiding the while() loop

Using while loops to check for anything is usually NOT recommended as it hangs uo the browser doesn't let the JavaScript thread do anyting else (all browsers nowadays are multithreaded, see this comic).

Here's Chuck suggested, this solution is much better:

function checkStyle(elem, prop, callback) {
    if ( getStyle(elem, prop) !== 'computed' ) {
        // see explanation below
        window.setTimeout( function() {checkStyle(elem, prop, callback);}, 100 )
    } else {
        callback()
    }
}

This is much better. Now the function asynchronously checks every 100ms (you can change this value if you want). The wrapping of the function while setting the timeout was necessary as setTimeout() doesn't allow passing of any arguments yet.

Hope this helped. See the source code of this post for some more links...

Community
  • 1
  • 1
Yatharth Agarwal
  • 4,385
  • 2
  • 24
  • 53
  • You can already pass arguments from the current scope to setTimeout() ...but with such a weird syntax it's often impractical. For example to pass argument from the current scope: foo='bar'; setTimeout('myfunc('+foo+')', 100); The arguments are evaluated immediately and in the current scope, not at the time myfunc(...) is executed nor in the execution scope. – Chuck Kollars Oct 02 '12 at 15:43
  • @ChuckKollars Why? Why would they do such a thing? The language designers are evil! Even though your previous solution was longer, it was less arcane and more readable/Googleable... – Yatharth Agarwal Oct 02 '12 at 15:54
  • @ChuckKollars Thanks, super-interesting. What is that double plus sign `+foo+` syntax/operator called? Do you have a good documented reference for it? – Ben Oct 04 '12 at 23:39
1

You can use DOMContentLoaded event. It fires when dom tree is ready and styles applyed. window.load fires, when all images and scripts loaded.

if you using jQuery, you can use this:

$(function(){
 // this fn not waiting for images
  $('.selector-class')[0].style ;// this is your computed style OR...
  $('.selector-class').css(); // json representation of style;
});

if you have no jQuery on page ... in modern browsers you can use this code:

document.addEventListener( "DOMContentLoaded", function(){
  var computedStyle = document.querySelector('#yourElementId').style ; 
})

YatharthROCK Addedd very precission solution below.

Maxim Antonov
  • 295
  • 3
  • 9
  • Thanks, but you're missing the question slightly. DOMContentLoaded (same as jQuery window.load on modern browsers) takes way too long on my page, over thirty seconds. It's unusable. That's the whole premise of the question. – Ben Sep 26 '12 at 18:36
  • DOMContentLoaded doesn't wait for stylesheets to be loaded. https://developer.mozilla.org/en-US/docs/DOM/DOM_event_reference/DOMContentLoaded – Kevin B Sep 26 '12 at 18:36
  • Right, but it does wait for images, which in this case is way too long. – Ben Sep 26 '12 at 19:05
  • @Maxim Cool, didn't know it was so easy with jQuery. It really fixes a lot of problems with the horrible DOM API... – Yatharth Agarwal Sep 27 '12 at 11:41
  • @Maxim. Your revised answer using (document).ready doesn't work either, for all the reasons outline in my original question. Computed styles are not available at (document).ready, because layout has not been performed yet. A paragraph with no explicit CSS height _will not have a computed height_ at document.ready. Every once in a while it will work as a fluke, sometimes on reloaded pages, in some browsers, and depending on the nature of the page, but it doesn't work at all reliably. – Ben Sep 27 '12 at 17:27
0

My other answer only told you when a CSS file had been downloaded, while in your question you mention you need to know when a style for an element has been computed or rendered or painted- has been updated.

I hope this addresses the issue (I didn't want to merge this with the other answer as I felt it attacked a different angle...) I changed my mind, see the edit.

Source: http://atomicrobotdesign.com/blog/javascript/get-the-style-property-of-an-element-using-javascript/

Add a unique reference styles to the CSS files you want to check for like so:

#ensure-cssload-0 {
  ignored-property: 'computed';
}

Now, use getPropertyValue and getComputedStyle to check for the CSS:

function getStyle(elem, prop) {
    return window.getComputedStyle(elem, null).getPropertyValue(prop);
}

Now, just use a while loop with a callback:

function checkStyle(elem, prop, callback) {
    while ( getStyle(elem, prop) !== 'computed' ) {
        // see explanation below
    }
    callback()
}

Since JavaScript doesn't have a pass statement (like in Python), we use an empty code block.

If you want to retrieve all the CSS styles applied to an element (warning: this will also show inherited styles), check out this SO answer

Community
  • 1
  • 1
Yatharth Agarwal
  • 4,385
  • 2
  • 24
  • 53
  • Thanks--but can you explain the empty while block? I've seen this kind of code structure before but I honestly don't get it. :) – Ben Sep 27 '12 at 00:48
  • @Ben As long as the condition fails (i.e., the CSS hasn't been computed), the while block executrs the code inside it (read: doesn't do anything). As soon as the condition is met (i.e., the CSS has been computed), the control flow can move on to execute the callback. Thus: whatever code is in the callback function gets executed right after the CSS is computed. Did that help? – Yatharth Agarwal Sep 27 '12 at 11:18
  • Okay, so it's kind of like an event listener. The problem that I see – Ben Sep 27 '12 at 17:13
  • oops... Okay, so the goal is to create something like an event listener. But doesn't the false `while` loop totally block execution? It seems like when the browser hits this `while` loop, it's going to go in circles, checking and rechecking, blocking the UI thread until the style returns as computed. Seems like bad architecture. How could I do this asychronously? Maybe I'm misunderstanding? Thanks so much... – Ben Sep 27 '12 at 17:29
  • @Ben Exactly. That's why I wasn't so enthusiastic about advocating this technique. From my other answer: _"I don't recommend this for the same reason why you shouldn't use a while loop to implement sleep() (it hangs up the browser), but if you must, then here it goes:"_ – Yatharth Agarwal Sep 28 '12 at 20:34
  • Instead of "function xxx() {while(notreadyyet) { }; dosomething();}", code "function xxx() {if (notreadyyet) setTimeout('xxx()', 100); dosomething();}". You get almost the same functionality, but check only every 100 millis (or a different time if you prefer) rather than the silly permanent looping tying up Javascript. I do this all the time with init events/sequences that are more ill-defined than I'd prefer. – Chuck Kollars Sep 28 '12 at 23:46
  • @Ben See my edit reflecting Chuck's idea. Now that it's ansynchronous, you can use it safely and I can get my accept :) – Yatharth Agarwal Sep 29 '12 at 10:13