javascripttimeoutsettimeout

find the time left in a setTimeout()?


I'm writing some Javascript that interacts with library code that I don't own, and can't (reasonably) change. It creates Javascript timeouts used for showing the next question in a series of time-limited questions. This isn't real code because it is obfuscated beyond all hope. Here's what the library is doing:

....
// setup a timeout to go to the next question based on user-supplied time
var t = questionTime * 1000
test.currentTimeout = setTimeout( showNextQuestion(questions[i+1]), t );

I want to put a progress bar onscreen that fills towards questionTime * 1000 by interrogating the timer created by setTimeout. The only problem is, there seems to be no way to do this. Is there a getTimeout function that I'm missing? The only information on Javascript timeouts that I can find is related only to creation via setTimeout( function, time) and deletion via clearTimeout( id ).

I'm looking for a function that returns either the time remaining before a timeout fires, or the time elapsed after a timeout has been called. My progress bar code looks like this:

var  timeleft = getTimeout( test.currentTimeout ); // I don't know how to do this
var  $bar = $('.control .bar');
while ( timeleft > 1 ) {
    $bar.width(timeleft / test.defaultQuestionTime * 1000);
}

tl;dr: How do I find the time remaining before a javascript setTimeout()?


Here's the solution I'm using now. I went through the library section that's in charge of tests, and unscrambled the code (terrible, and against my permissions).

// setup a timeout to go to the next question based on user-supplied time
var t = questionTime * 1000
test.currentTimeout = mySetTimeout( showNextQuestion(questions[i+1]), t );

and here's my code:

// wrapper for setTimeout
function mySetTimeout( func, timeout ) {
    timeouts[ n = setTimeout( func, timeout ) ] = {
        start: new Date().getTime(),
        end: new Date().getTime() + timeout
        t: timeout
    }
    return n;
}

This works pretty spot-on in any browser that isn't IE 6. Even the original iPhone, where I expected things to get asynchronous.


Solution

  • If you can't modify the library code, you'll need to redefine setTimeout to suit your purposes. Here's an example of what you could do:

    (function () {
    var nativeSetTimeout = window.setTimeout;
    
    window.bindTimeout = function (listener, interval) {
        function setTimeout(code, delay) {
            var elapsed = 0,
                h;
    
            h = window.setInterval(function () {
                    elapsed += interval;
                    if (elapsed < delay) {
                        listener(delay - elapsed);
                    } else {
                        window.clearInterval(h);
                    }
                }, interval);
            return nativeSetTimeout(code, delay);
        }
    
        window.setTimeout = setTimeout;
        setTimeout._native = nativeSetTimeout;
    };
    }());
    window.bindTimeout(function (t) {console.log(t + "ms remaining");}, 100);
    window.setTimeout(function () {console.log("All done.");}, 1000);
    

    This is not production code, but it should put you on the right track. Note that you can only bind one listener per timeout. I haven't done extensive testing with this, but it works in Firebug.

    A more robust solution would use the same technique of wrapping setTimeout, but instead use a map from the returned timeoutId to listeners to handle multiple listeners per timeout. You might also consider wrapping clearTimeout so you can detach your listener if the timeout is cleared.