2

Have a look at the following code (additionally, you will need jquery.js, jquery.viewport.js and jquery.scrollTo.js).

The behaviour I would expect from this script is that whenever I scroll the page, the red rows (<tr> elements with class alwaysVisible) should be inserted just underneath the top-most visible row (<tr> element) of that table. Then, the page should be scrolled so that the first of these red rows appears "exactly" at the top of the viewport. What actually happens is that makeVisibleWhatMust(); is called repeatedly until I reach the end of the page. I thought $(window).unbind('scroll'); would keep makeVisibleWhatMust(); from being called again, but apparently this doesn't work.

Any ideas why?

Here is the JavaScript I wrote:

function makeVisibleWhatMust()
{
  $('#testContainer').text( $('#testContainer').text() + 'called\n');
  $('table.scrollTable').each
  (
    function()
    {
      var table = this;
      $($('tr.alwaysVisible', table).get().reverse()).each
      (
    function()
    {
      $(this).insertAfter( $('tr:in-viewport:not(.alwaysVisible)', table)[0] );
    }
      );
      $(window).unbind('scroll');
      $(window).scrollTo( $('tr.alwaysVisible')[0] );
      $(window).bind('scroll', makeVisibleWhatMust);
    }
  );
}

$(document).ready
(
  function()
  {
    $(window).bind('scroll', makeVisibleWhatMust);
  }
);

And here is an HTML page to test it on:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
  <head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
    <title>Scroll Tables Test Page</title>

    <script type="text/javascript" src="jQuery.js"></script>
    <script type="text/javascript" src="jquery.viewport.js"></script>
    <script type="text/javascript" src="jquery.scrollTo.js"></script>
    <script type="text/javascript" src="scrollTable.js"></script>

    <style type="text/css">
      table th, table td
      {
    border: 1px solid #000;
    padding: .3em;
      }
      .alwaysVisible
      {
    background: #F66;
      }
    </style>
  </head>
  <body>
    <table class="scrollTable">
      <thead>
    <tr class="alwaysVisible">
      <th>Row name</th>
      <th>Content</th>
    </tr>
    <tr class="alwaysVisible">
      <th>Row 2</th>
      <th>Row 2</th>
    </tr>
      </thead>
      <tbody>
    <script type="text/javascript">
      for(var i = 0; i < 50; ++i)
      {
        document.writeln("<tr><td>Row " + i + "</td><td>Content</td></tr>");
      }
    </script>
      </tbody>
      <tfoot>
    <tr>
      <td>Footer</td>
      <td>Footer 2</td>
    </tr>
      </tfoot>
    </table>
    <div id="testContainer">TEST CONTAINER</div>
  </body>
</html>
mu is too short
  • 426,620
  • 70
  • 833
  • 800
Shawn
  • 10,931
  • 18
  • 81
  • 126

1 Answers1

0

I think you're problem is that scrollTo uses animate:

// From the plugin's source
function animate( callback ){  
    $elem.animate( attr, duration, settings.easing, callback && function(){  
        callback.call(this, target, settings);  
    });  
};

And animate uses a timer to perform the animation. The result is that .scrollTo will return before the scrolling has completed and you'll rebind your scroll handler while the scrollTo is still scrolling. Hence the events when you're not expecting them.

An easy solution would be to use a flag to tell makeVisibleWhatMust that scrollTo is scrolling and use a scrollTo callback to clear the flag when it is done, something like this:

function makeVisibleWhatMust() {
  // Ignore the event if we're doing the scrolling.
  if(makeVisibleWhatMust.isScrolling)
    return;
  $('#testContainer').text( $('#testContainer').text() + 'called\n');
  $('table.scrollTable').each(function() {
      var table = this;
      $($('tr.alwaysVisible', table).get().reverse()).each(function() {
        $(this).insertAfter( $('tr:in-viewport:not(.alwaysVisible)', table)[0] );
      });
      makeVisibleWhatMust.isScrolling = true;
      $(window).scrollTo($('tr.alwaysVisible')[0], {
        onAfter: function() { makeVisibleWhatMust.isScrolling = false; }
      });
    }
  );
}
makeVisibleWhatMust.isScrolling = false;

And here's a live version that seems to work: http://jsfiddle.net/ambiguous/ZEx6M/1/

mu is too short
  • 426,620
  • 70
  • 833
  • 800
  • @mu is too short : I think you're right about the problem, but your solution doesn't work. I get exactly the same result as before. Now I really don't understand.. – Shawn Jun 01 '11 at 20:40
  • @Shawn: I had a typo in the JavaScript (too many closing parentheses). I fixed that and added a jsfiddle link. – mu is too short Jun 01 '11 at 20:55
  • @mu is too short : So it does work! That is very curious because the exact same code doesn't work when I test it "locally", but does work in JSFiddle. I wonder what can account for this difference of behavior between my "local setup" (saving code in text files and opening the html file in Firefox) and JSFiddle (also running in Firefox).. I guess this issue deserves a question of its own. Anyways, thanks for the (working) solution! – Shawn Jun 01 '11 at 21:06
  • @Shawn: Does the fully unwrapped version of the fiddle work? Try this: http://fiddle.jshell.net/ambiguous/ZEx6M/1/show/light/ – mu is too short Jun 01 '11 at 21:09
  • @mu is too short : Yeah that works perfectly (or at least, it works as expected! I do think the final result is a bit choppy, but that's another story :p) – Shawn Jun 01 '11 at 21:18
  • I started a new question regarding this JSFiddle problem: http://stackoverflow.com/questions/6207907/what-are-possible-causes-for-differing-behaviors-in-jsfiddle-and-in-local-context – Shawn Jun 01 '11 at 21:26
  • @mu : I'm having problems with your solution after all! It seems the `onAfter` statement sets `isScrolling` to false before `makeVisibleWhatMust` tests it (following scrollTo's scroll event).. At least that's how I interpret the results of the following test: Go [here](http://jsfiddle.net/95muf/) then scroll down ONCE (one scroll-wheel-click towards the bottom). You will see the "log" function being called twice, which is normal since you scrolled and the script scrolled. What is strange is that even the second time, `isScrolling` is shown to be `false`. Tell me if this is unclear.. – Shawn Jun 13 '11 at 20:26
  • Sorry, use this link instead: http://jsfiddle.net/95muf/1/ I'm not too good with JSFiddle yet.. – Shawn Jun 13 '11 at 20:32
  • I think the non-elegant solution of replacing the `onAfter` statement with `onAfter: function() { window.setTimeout(function(){ makeVisibleWhatMust.isScrolling = false; }, 200); }` proves my theory.. Any better ideas? – Shawn Jun 13 '11 at 20:43
  • @Shawn: Your 95muf version doesn't work for me, it keeps jumping back up. This one (which has your "LOG" change merged in) works fine for me: http://fiddle.jshell.net/ZEx6M/7/ – mu is too short Jun 13 '11 at 21:02
  • @mu is too short : ZEx6M/7 works fine for me too. But as I said, the word `false` appears twice even if I scroll only once. Or, if I use the fix in my previous comment (with setTimeout), then I get `false`, `true` instead. Do you get the same results? – Shawn Jun 14 '11 at 12:44
  • @Shawn: I get "false, true, true" from ZEx6M/7 – mu is too short Jun 14 '11 at 20:13
  • What? with a single scroll? What happens when you use the fix? Still `false`, `true`, `true` ? – Shawn Jun 14 '11 at 21:31
  • @Shawn: I get false/true/true from ZEx6M/7 with a single click on the scroll bar's down arrow. – mu is too short Jun 15 '11 at 00:08