Blocking events with blocker lists
It’s often useful to be able to detect scroll events using the onscroll event handler in JavaScript. For example, every time a user scrolls to nearly the bottom of the page, you load more content to create an ‘endless’ page. In my case, I have two DIVs set to overflow: auto, with chat histories in each, where the chats have been taking place simultaneously. I want to detect when the user scrolls one of the DIVs (either one) and then scroll the other one to keep the two in sync. That is to say, we want messages that are currently vertically in the middle of DIV 1 to have been posted at around the same time as the messages at the same vertical viewport offset in DIV 2.
Try scrolling either of the panels in this example:
You should find it scrolls madly and unpredictably until you press stop.
The first problem is that onscroll is not simply fired when you finish scrolling. It fires like a machine gun continuously while the mouse cursor is moving on the scroll handle. The solution to this is a watchdog. A watchdog is a timer that will execute action A if action B does not happen within X seconds. So every time onscroll fires, we reset the timer, and if it gets to zero we know that the user has finished scrolling (or at least has paused for long enough for us to do something about it).
You’ll find that although it’s not quite as crazed, the panels do keep scrolling, one after the other.
The problem now is that when you scroll DIV 1, and this causes an automatic scroll of DIV 2, that automatic scroll ALSO triggers the onscroll event and so we then scroll DIV 1 again. The solution is to have a variable that flags whether we are currently paying attention to scroll events, and turn it off when we don’t want to detect scrolling (ie just before the auto-scroll) and then on again after the scroll has completed (I’m using an animation library so the scroll takes around half a second to complete). Try this (don’t click the button yet, just scroll the panels):
Success. However, this approach is a bit short-sighted.
You also need to turn off scroll detection when adding or removing content from either DIV, because changing the height of the content within the element can also fire the onscroll event. So if this is a chat window, and we’re adding and removing content in the DIVs, we have to disable scroll detection while we’re doing that and then turn it on again afterwards.
Sadly, our scroll detection flag is crude – it’s a sheer yes/no, and if it’s already set to no (say in order to execute a lengthy scrolling animation), and in the meantime we need to do something quick (say adding a line to one of the DIVs) then that quick operation is going to disable scroll detection (unnecessarily, as it’s already disabled) and then crucially enable it again before the scroll animation has finished. Click the button in the example above to demonstrate this (and then scroll). You should, intermittently, get the unwanted cascade effect you saw in example 2.
What we need is a list, not a flag. Enter the blocker list – an object that collates tokens from each procedure in the script that is currently blocking scroll detection. So if a procedure wants to disable scrolling for a period of time, it adds its token to the blocker list, and then removes its token when it’s done. When a scroll event fires, we now only need to know whether there are any items in the blocker list, and then we can work out if it’s ok to process the event.
It’s also worth noting that there’s no need to actually count the number of items in the blocker list – it’s enough simply to know that there is at least one item in there. And as a further optimisation, we don’t actually need to compute this when scroll events fire (frequently), because the value will only change when some function wants to add or remove its token from the list (less frequent). So we can have enableScrollDetection and disableScrollDetection functions that deal with the blocker list, and which ultimately simply change the old scrolldetection flag to true or false.
There might be a neater way of achieving this, but this certainly works for me. I’d welcome any comments or suggestions. The sources for all these demos are available in the iframes above.
2 Comments
Is there a reason why you use a token list, and not just a counter?
Start with the counter at 0. Increase the counter by one every time you start doing something, and decrease the counter by one when you finish something. As long as the counter is zero you can sync scrolling.
Incrementing/decrementing a counter is really fast, and checking if a counter is 0 is also very fast.
Jakob – In our particular use case we wanted to know what was blocking the scroll, and it was also extremely helpful for debug. But you’re right – in a simple use case like the one described in the post, a counter would work fine.