1

I'm using the following event listener to detect mouse wheel and scroll direction:

window.addEventListener('wheel', ({ deltaY }) => {
  console.log(deltaY);
  if (deltaY > 0) scrollDown();
  else if (deltaY < 0) scrollUp();
});

The following happens here:

  • 2 finger touch pad scroll on Macbook triggers the event handler
  • deltaY keeps logging due to the scroll accelerometer
  • scrollDown() or scrollUp() keep firing until accelerometer stops

I only want to fire scrollUp and scrollDown once per user interaction. I therefore need to detect a new mouse scroll event, not every mouse scroll events. Is this possible?

I did try a timeout to detect if deltaY was still changing due to the accelerometer, but this wasn't sufficient because if it was still changing, a second user interaction did not trigger scrollUp or scrollDown.

Here's a CodePen of what I'm trying to achieve: https://codepen.io/anon/pen/dQmPNN

It's very close to the required functionality, but if you hammer the mouse wheel hard on the first slide, then try to scroll to the next one immediately, the timeout solution locks it so you have to wait another second or so until the timeout completes and you can continue scrolling.

CaribouCode
  • 13,998
  • 28
  • 102
  • 174
  • Show us what you tried with the timeout then. You will need to either throttle or debounce the event handling, see here for the difference between the two methods: https://stackoverflow.com/questions/25991367/difference-between-throttling-and-debouncing-a-function – misorude Nov 22 '18 at 12:05
  • @misorude Happy to share my timeout attempt if you think it's still relevant, but the point I'm making is that a simple debounce or throttle isn't sufficient here because neither actually detect a new user interaction, they would just wait until the accelerometer has finished, then allow another interaction. – CaribouCode Nov 22 '18 at 12:07
  • Ah, so you want them to keep scrolling, but fire your up/down functions only once when they start scrolling? Then you should remember what direction they were scrolling in previously, and compare that to the direction in the current handler execution. – misorude Nov 22 '18 at 12:11

3 Answers3

2

This is old but I found it when looking for an answer to pretty much the same problem. I solved the problem for my purposes, so here's my solution in case it helps anyone else.

The problem is really to define what counts as one continuous action. Without something more concrete to work with, it's just a question of timing. It's the time between events that's the key - so the algorithm is to keep accumulating events until there's a certain gap between them. All that then remains is to figure out how big the allowed gap should be, which is solution specific. That's then the maximum delay after the user stops scrolling until they get feedback. My optimum is a quarter of a second, I'm using that as a default in the below.

Below is my JavaScript, I'm attaching the event to a div with the id 'wheelTestDiv' using jQuery but it works the same with the window object, as in the question.

It's worth noting that the below looks for any onWheel event but only tracks the Y axis. If you need more axes, or specifically only want to count events towards the timer when there's a change in deltaY, you'll need to change the code appropriately.

Also worth noting, if you don't need the flexibility of tracking events against different DOM objects, you could refactor the class to have static methods and properties, so there would be no need to create a global object variable. If you do need to track against different DOM objects (I do), then you may need multiple instances of the class.

"use strict";
class MouseWheelAggregater {
    // Pass in the callback function and optionally, the maximum allowed pause
    constructor(func, maxPause) {
        this.maxAllowedPause = (maxPause) ? maxPause : 250; // millis
        this.last = Date.now();
        this.cummulativeDeltaY = 0;
        this.timer;
        this.eventFunction = func;
    }
    
    set maxPause(pauseTime) {
        this.maxAllowedPause = pauseTime;
    }

    eventIn(e) {
        var elapsed = Date.now() - this.last;
        this.last = Date.now();
        if ((this.cummulativeDeltaY === 0) || (elapsed < this.maxAllowedPause)) {
            // Either a new action, or continuing a previous action with little
            // time since the last movement
            this.cummulativeDeltaY += e.originalEvent.deltaY;
            if (this.timer !== undefined) clearTimeout(this.timer);
            this.timer = setTimeout(this.fireAggregateEvent.bind(this), 
                this.maxAllowedPause);
        } else { 
            // just in case some long-running process makes things happen out of 
            // order
            this.fireAggregateEvent();
        }
    }

    fireAggregateEvent() {
        // Clean up and pass the delta to the callback
        if (this.timer !== undefined) clearTimeout(this.timer);
        var newDeltaY = this.cummulativeDeltaY;
        this.cummulativeDeltaY = 0;
        this.timer = undefined;
        // Use a local variable during the call, so that class properties can
        // be reset before the call.  In case there's an error.
        this.eventFunction(newDeltaY);
    }
}

// Create a new MouseWheelAggregater object and pass in the callback function,
// to call each time a continuous action is complete.
// In this case, just log the net movement to the console.
var mwa = new MouseWheelAggregater((deltaY) => {
    console.log(deltaY);
});

// Each time a mouse wheel event is fired, pass it into the class.
$(function () {
    $("#wheelTestDiv").on('wheel', (e) => mwa.eventIn(e));
});

Web page ...

<!DOCTYPE html>
<html>
  <head> 
    <title>Mouse over test</title>
    <script src="/mouseWheelEventManager.js"></script>
  </head> 
  <body>
    <div id="wheelTestDiv" style="margin: 50px;">Wheel over here</div>
  </body>
</html>
Kipperfer
  • 108
  • 1
  • 9
0

Have you tried breaking this out into functions with a flag to check if an interaction has occurred?

For example:

// Create a global variable which will keep track of userInteraction
let shouldScroll = true;

// add the event listener, and call the function when triggered
window.addEventListener('wheel', () => myFunction());

//Create a trigger function, checking if shouldScroll is true or false.
myFunction(){
    shouldScroll ? (
        if (deltaY > 0) scrollDown();
        else if (deltaY < 0) scrollUp();
        // Change back to false to prevent further scrolling. 
        shouldScroll = false;
    ) : return;
}

/* call this function when user interaction occurs
 and you want to allow scrolling function  again.. */
userInteraction(){
    // set to true to allow scrolling
    shouldScroll = true;
}
Luke Walker
  • 511
  • 5
  • 19
  • This is kind of similar to the timeout solution I had created. How would you detect whether the next deltaY change is from the previous scroll's accelerometer or from a new user interaction though? That's the problem. – CaribouCode Nov 22 '18 at 12:17
  • Checkout the CodePen I've added to my original question to demonstrate the problem. – CaribouCode Nov 22 '18 at 12:39
0

We can avoid such a situation by delay execution and removing the events in between the delay, refer below example and have added 1000ms as delay which can be modified based on your requirements.

        let scrollPage = (deltaY)=>{
        console.log(deltaY);
        if (deltaY > 0) scrollDown();
        else if (deltaY < 0) scrollUp();
        };

        var delayReg;
        window.addEventListener('wheel', ({ deltaY }) => {
            clearTimeout(delayReg);
            delayReg = setTimeout(scrollPage.bind(deltaY),1000);
        });
Manivannan
  • 3,074
  • 3
  • 21
  • 32
  • Yep, this is pretty much exactly the timeout solution I tried. The issue here is, any new user scroll interaction should trigger the intended functionality. In this case, if the user scrolls again within 1000ms of the accelerometer finishing, it won't trigger `scrollDown` or `scrollUp` functions. – CaribouCode Nov 22 '18 at 12:23
  • i.e. if you have a very sensitive mouse wheel and really hammer it hard the first time, the accelerometer could keep going for a couple of seconds, so the user is locked from interacting with another scroll for those 2 seconds plus the 1000ms timeout. So they can't do anything for 3s. – CaribouCode Nov 22 '18 at 12:25
  • I've added a CodePen to my original post of what I'm attempting to do, and the timeout implementation, with a further explanation of the problem. – CaribouCode Nov 22 '18 at 12:38
  • @Coop oh, got your point. AFAIK there is no better solution than using time out. – Manivannan Nov 22 '18 at 12:53
  • I thought that might be the case but I wonder how some sites handle layouts like this, where they hijack the scrolling. – CaribouCode Nov 22 '18 at 13:02