检测滚动事件是否由用户创建

是否可以判断滚动事件是由浏览器完成的还是由用户完成的?具体来说,当使用后退按钮时,浏览器可能会跳转到最后已知的滚动位置。如果我绑定到滚动事件,我如何才能知道这是由用户还是浏览器造成的?

$(document).scroll( function(){
//who did this?!
});

我看到了三种导致在浏览器中滚动的情况。

  1. 用户执行一些操作。例如,使用鼠标轮、箭头键、页面向上/向下键、主/结束键、单击滚动条或拖动它的拇指。
  2. 浏览器会自动滚动。例如,当在浏览器中使用后退按钮时,它会自动跳到最后一个已知的滚动位置。
  3. Javascript 卷轴,例如,element.scrollTo(x,y)
56453 次浏览

Try using the Mousewheel and DOMMouseScroll events instead. See http://www.quirksmode.org/dom/events/scroll.html

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.

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 is false
  • Scroll using mouse/keyboard - userScroll becomes true
  • Click on the link to jump to page bottom - userScroll becomes false
  • Click Back/Forward - userScroll becomes false;

<!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!

You can check the scroll position on ready. When you fire the on scroll event check to make sure the scroll position is different than it was when the page loaded. Lastly be sure to clear out the stored value once the page is scrolled.

$(function () {
var loadScrollTop = ($(document).scrollTop() > 0 ? $(document).scrollTop() : null);
$(document).scroll(function (e) {
if ( $(document).scrollTop() !== loadScrollTop) {
// scroll code here!
}
loadScrollTop = null;
});
});

Found this very useful. Here's a coffeescript version for those so inclined.

$ ->
S.userScroll = false


# reset flag on back/forward
if $.history?
$.history.init (hash) ->
S.userScroll = false


mouseEvent = (e)->
S.userScroll = true


$(document).keydown (e) ->
importantKey =  (e.which == 33 or        # page up
e.which == 34 or    # page dn
e.which == 32 or    # spacebar
e.which == 38 or    # up
e.which == 40 or    # down
(e.ctrlKey and e.which == 36) or    # ctrl + home
(e.ctrlKey and e.which == 35)    # ctrl + end
)
if importantKey
S.userScroll = true;


# Detect user scroll through mouse
# Mozilla/Webkit
if window.addEventListener
document.addEventListener('DOMMouseScroll', mouseEvent, false);


# for IE/OPERA etc
document.onmousewheel = mouseEvent;

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)) {
}
}
});

if you're using JQuery than there's a better answer, apparently - i'm just trying it out myself.

see: Detect jquery event trigger by user or call by code

Regarding to:

Specifically, when using the back button a browser may jump to the last known scroll position.

That fires very soon, after the page is rendered. You can just delay listenting to the scroll event by 1 second or so.

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.

There is one more way to separate the user-created scroll: you can use the alternative event handlers, for example 'mousewheel', 'touchmove', 'keydown' with codes 38 and 40 for arrow scrolling, for scrolling with scroll bar - if 'scroll' event is fired simultaneously with 'mousedown' until 'mouseup' event.

It might not help with your application, but I needed to fire an event on user scroll but not programatic scroll and posting incase it helps anyone else.

Listen to the wheel event instead of scroll,

It is triggered whenever a user uses the mouse wheel or tracker pad( which I feel is how most people scroll anyway) and isn't fired when you programatically scroll. I used it to differentiate between a user scroll action and scrolling functions.

https://developer.mozilla.org/en-US/docs/Web/API/Element/wheel_event


element.addEventListener('wheel', (event) => {
//do user scroll stuff here
})


One caveat is that wheel doesn't fire on scroll on mobile, so I checked whether the device was mobile and used similar functions

if(this.mobile){
element.addEventListener('scroll', (event) => {
//do mobile scroll stuff here
})
}