2

I have been struggling to figure out what I am doing wrong here. I am simply trying to change the cursor to "progress" using jQuery's .css() function after the user clicks a button but before looping through a series in HighStock (that takes a couple of seconds to execute) and showing/hiding some lines (32 of them!).

I have tried many things but no matter what I try it simply freezes the button in the "down" position (not changing the cursor, but you can still move it) until the code is finished.

Here's what I've tried (in order):

Note: All of my attempts produce the exact same results (i.e., no errors are produced, the code gets executed, but I never get the "progress" cursor to show while code is busy being executed.

$("#onAllOverall").click(function () {
    $("body").css("cursor", "progress");

    for (var s = 0; s < series.length; s++) {
        series[s].hide();
    }

    $("body").css("cursor", "default");
});

So then, I tried:

$("#onAllOverall").click(function () {
    $("body").css("cursor", "progress");
}

$("#onAllOverall").click(function () {
    for (var s = 0; s < series.length; s++) {
        series[s].hide();
    }

    $("body").css("cursor", "default");
});

I even went so far as to try:

function progressCursor() {
    $("body").css("cursor", "progress");
}

$("#onAllOverall").click(function () {

    $.when(progressCursor).done(function () { //also tried $.when.then(), but I admit I don't know much about these methods;
        for (var s = 0; s < series.length; s++) {
            series[s].show();
        }
    });

    $("body").css("cursor", "default");
});

I remember trying a couple of other things but I can't rightly remember what they were, but they were basic and unhelpful.

I feel kind of silly that nothing is working here. What am I doing wrong?

VoidKing
  • 6,282
  • 9
  • 49
  • 81

1 Answers1

4

You will need to "yield" the event loop to the browser so it can render the page (including cursor change) during your for loop.

The easiest way to yield in a browser is with setTimeout of 0 ms. (Nodejs has a function called nextTick which is actually what you want, but browsers haven't implemented it yet.)

$("#onAllOverall").click(function () {
    $("body").css("cursor", "progress");
    var s = 0;
    function next(){
        series[s].hide();
        s++;
        if(s < series.length){
            setTimeout(next, 0);
        } else {
            $("body").css("cursor", "default");
        }
    }
    if(series.length){
        next();
    }           
});

The https://github.com/caolan/async library is excellent for these things.

Note that this implementation yields after every hide() event. You may find that it works with only a single set-timeout wrapping the for loop. Yielding after every operation will make the page more responsive while the loop is happening, but also may have the page looking strange as each element will disappear one at a time.

To have it not yield after every action:

$("#onAllOverall").click(function () {
    $("body").css("cursor", "progress");

    function action(){
       for (var s = 0; s < series.length; s++) {
          series[s].hide();
       }

       $("body").css("cursor", "default");
    }
    setTimeout(action, 0);       
});
Manfred
  • 5,320
  • 3
  • 35
  • 29
Will Shaver
  • 12,471
  • 5
  • 49
  • 64
  • Okay, I tried this and it works, but I'm not 100% sure I know exactly why. After seeing it in action, I can tell that I would rather it not yield after every `hide()` event. Can you show an example where it works with only a single set-timeout wrapping the for loop? – VoidKing Mar 27 '14 at 16:06
  • If series.length is zero you wouldn't want to run the loop. if(series.length) ensures it is greater than zero. – Will Shaver Mar 27 '14 at 16:10
  • Yeah, so sorry, I saw it just after I posted, then deleted the comment :) Thanks for the explanation and thank you so much for the other example. You've got it working for me, but what I really really appreciate is learning from your examples and *why* they worked. Thanks again! – VoidKing Mar 27 '14 at 16:11
  • So effectively, all you're doing here is turning the `action` into an asynchronous function? Is that correct? – VoidKing Mar 27 '14 at 16:12
  • Oh, no, you're not going to believe this, but your second example (the one I need) doesn't work. It still just locks up the interface and doesn't show the progress cursor. – VoidKing Mar 27 '14 at 16:14
  • I highly recommend you read up on the javascript event loop. You need to yield to the browser so it can render. It takes a turn along with all your javascript code. – Will Shaver Mar 27 '14 at 16:14
  • Try adjusting the timeout. – Will Shaver Mar 27 '14 at 16:15
  • Okay, well this is odd, but it only works if I move the mouse in the amount of milliseconds specified in the timeout. If I hold the mouse still it will stay frozen, even if I move it after the timeout event has fired. Is there any solution to this? – VoidKing Mar 27 '14 at 16:30
  • Sadly, no. http://stackoverflow.com/questions/1718415/getting-the-browser-cursor-from-wait-to-auto-without-the-user-moving-the-mou I'd recommend changing something else on the page instead of the cursor. – Will Shaver Mar 27 '14 at 16:36
  • I guess you're right, that's probably best, but your first option is looking more appealing now that I know what work I would have to put into the second one, lol. – VoidKing Mar 27 '14 at 17:07