Detect whether scroll event was created by user
Is it possible to tell whether a scroll event was done by the browser or by the user? Specifically, when using the back button a browser may jump to the last known scroll position. If I bind to scroll event how can I tell whether this was caused by user or browser?
$(document).scroll( function(){
//who did this?!
});
I see three types of situations that cause scrolling in a browser.
- The user performs some action. For example, uses mousewheel, arrow keys, page up/down keys, home/end keys, clicks the scrollbar or drags its thumb.
- The browser scrolls automatically. For example, when using the back button in your browser it will jump to the last known scroll position automatically.
- Javascript scrolls. For example,
element.scrollTo(x,y)
.
Unfortunately, there is no direct way of telling that.
I would say if you can redesign your app so that it doesn't depend on this type of flow, go for that.
If not, a workaround I can think of is to keep track of user initiated scrolls and check that to see if the scroll was triggered by the browser or by the user.
Here's an example that I put together which does this pretty well (except for browsers where jQuery history has problems with).
You need to run this locally to be able to test it fully (jsFiddle/jsbin are not good fits as they iFrame the contents).
Here's the test cases that I validated:
- Page loads -
userScroll
isfalse
- Scroll using mouse/keyboard -
userScroll
becomestrue
- Click on the link to jump to page bottom -
userScroll
becomesfalse
- Click Back/Forward -
userScroll
becomesfalse
;
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<script src="http://code.jquery.com/jquery-1.6.1.min.js"></script>
<script type="text/javascript" src="https://raw.github.com/tkyk/jquery-history-plugin/master/jquery.history.js"></script>
</head>
<body>
<span> hello there </span><br/>
<a href="#bottom"> click here to go down </a>
<br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/>
<br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/>
<br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/>
<br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/>
<br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/>
<br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/>
<br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/>
<a name="bottom"> just sitting </a>
</body>
<script type="text/javascript">
var userScroll = false;
function mouseEvent(e) {
userScroll = true;
}
$(function() {
// reset flag on back/forward
$.history.init(function(hash){
userScroll = false;
});
$(document).keydown(function(e) {
if(e.which == 33 // page up
|| e.which == 34 // page dn
|| e.which == 32 // spacebar
|| e.which == 38 // up
|| e.which == 40 // down
|| (e.ctrlKey && e.which == 36) // ctrl + home
|| (e.ctrlKey && e.which == 35) // ctrl + end
) {
userScroll = true;
}
});
// detect user scroll through mouse
// Mozilla/Webkit
if(window.addEventListener) {
document.addEventListener('DOMMouseScroll', mouseEvent, false);
}
//for IE/OPERA etc
document.onmousewheel = mouseEvent;
// to reset flag when named anchors are clicked
$('a[href*=#]').click(function() {
userScroll = false;
});
// detect browser/user scroll
$(document).scroll( function(){
console.log('Scroll initiated by ' + (userScroll == true ? "user" : "browser"));
});
});
</script>
</html>
Notes:
- This doesn't track scrolling when the user drags the scrollbar with mouse. This can be added with some more code, which I left as an exercise for you.
-
event.keyCodes
may vary by OS, so you may have to change that appropriately.
Hope this helps!
Rather than trying to catch all the user events, it's much easier to do the opposite and handle only the programmatic events - and ignore those.
For example, this kind of code would work:
// Element that needs to be scrolled
var myElement = document.getElementById('my-container');
// Flag to tell if the change was programmatic or by the user
var ignoreNextScrollEvent = false;
function setScrollTop(scrollTop) {
ignoreNextScrollEvent = true;
myElement.scrollTop = scrollTop
}
myElement.addEventListener('scroll', function() {
if (ignoreNextScrollEvent) {
// Ignore this event because it was done programmatically
ignoreNextScrollEvent = false;
return;
}
// Process user-initiated event here
});
Then when you call setScrollTop()
, the scroll event will be ignored, while if the user scroll with the mouse, keyboard or any other way, the event will be processed.
As far as I know it is impossible (without any work) to tell whenever scroll event has been issued by "user" or by other means.
You could try (as others mentioned) catch mousewheel events, then probably trying to catch keydown event on any keys that can trigger scroll (arrows, space etc.) while checking what element is currently focused, since you for example can't scroll using arrow keys while typing in an input field. In general that would be complex and messy script.
Depending on situation you're dealing with you could I guess "revert the logic", and instead of detecting user issued scroll events just hook in into any scrolls made programatically and treat any scroll events not made by your code as made by an user. Like I said it depends on a situation, and what you're trying to achive.
Yes, it is 100% possible. I'm current using this in an application where IE is not a requirement - client facing only. When my Backbone app initiates an animation where scroll is changed - scroll occurs but does not trigger these event captures. This is tested in FF, Safari & Chrome latest.
$('html, body').bind('scroll mousedown wheel DOMMouseScroll mousewheel keyup', function(evt) {
// detect only user initiated, not by an .animate though
if (evt.type === 'DOMMouseScroll' || evt.type === 'keyup' || evt.type === 'mousewheel') {
// up
if (evt.originalEvent.detail < 0 || (evt.originalEvent.wheelDelta && evt.originalEvent.wheelDelta > 0)) {
// down.
} else if (evt.originalEvent.detail > 0 || (evt.originalEvent.wheelDelta && evt.originalEvent.wheelDelta < 0)) {
}
}
});