12

I would like to get one event only per scroll event

I try this code but it produces "wheel" as many times the wheel event is triggered. Any help? Thank you

window.addEventListener("wheel",
    (e)=> {
        console.log("wheel");
        e.preventDefault();
    },
    {passive:false}
    );

Use case (edit) I want to allow scrolling from page to page only - with an animation while scrolling. As soon I detect the onwheel event, I would like to stop it before the animation finishes, otherwise the previous onwheel continues to fire and it is seen as new event, so going to the next of the targeted page

My conclusion : It is not possible to cancel wheel events. In order to identify a new user wheel action while wheeling events (from a former user action) are on going, we need to calculate the speed/acceleration of such events

tit
  • 599
  • 3
  • 6
  • 25

4 Answers4

5

This is fairly simple problem, store anywhere the last direction and coditionally execute your code:

direction = '';
window.addEventListener('wheel',  (e) => {
    if (e.deltaY < 0) {
      //scroll wheel up
      if(direction !== 'up'){
        console.log("up");
        direction = 'up';
      }
    }
    if (e.deltaY > 0) {
      //scroll wheel down
      if(direction !== 'down'){
        console.log("down");
        direction = 'down';
      }
    }
  });

Anyway, the UX context should be defined. May be that throttling or debouncing your function will give better results in some scenarios.

Throttling

Throttling enforces a maximum number of times a function can be called over time. As in "execute this function at most once every 100 milliseconds."

Debouncing

Debouncing enforces that a function not be called again until a certain amount of time has passed without it being called. As in "execute this function only if 100 milliseconds have passed without it being called.

In your case, maybe debouncing is the best option.

Temporary lock the browser scroll

$('#test').on('mousewheel DOMMouseScroll wheel', function(e) {
    e.preventDefault();
    e.stopPropagation();

    return false;
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<div id="test">
  <h1>1</h1>
  <h1>2</h1>
  <h1>3</h1>
  <h1>4</h1>
  <h1>5</h1>
  <h1>6</h1>
  <h1>7</h1>
  <h1>8</h1>
  <h1>9</h1>
  <h1>10</h1>
</div>
Mosè Raguzzini
  • 15,399
  • 1
  • 31
  • 43
  • Both will not work since they do not stop the wheel events. The case usage has been added in the question – tit Sep 27 '19 at 14:44
  • Look at my new snippet, mate – Mosè Raguzzini Sep 27 '19 at 15:16
  • Note: you cannot "stop" wheel events. You can stop to call a function bound to the event, you can stop to listen to the event, you can prevent the event to scroll a browser element or propagating from that element, but the event will always occurs (because your input device actually just send it anytime yoy scroll) – Mosè Raguzzini Sep 27 '19 at 15:22
  • ok, thank you. So it is not possible to cancel wheel events, clea now. In order to identify a new user wheel action while wheeling events (former user action) are on going, we may need to calculate the speed and the acceleration of such events – tit Sep 28 '19 at 22:24
4

Event.preventDefault() tells the browser not to do the default predefined action for that event, such as navigating to a page or submitting the enclosing form, etc. It does not necessarily prevent events from firing.

Also, there is a difference between the wheel event and the scroll event. The wheel event is fired when the user rotates a wheel button, and the scroll event is fired when the target's scrollTop or scrollLeft property is changed due to the scroll position being changed.

When the user rotates the wheel button, the wheel event is fired before any scroll events that could be fired. However, the wheel event might not result in any scroll event simply because the pointer is not hovering on any element or the element is not scrollable at the moment.

To aggregate quickly repeated function calls to the event handler, you can debounce the event handler function. The idea is to wait a certain amount before committing to the action. When a function is debounced, it becomes a new function, when called, sets off a timer that calls the wrapped function inside. The timer is reset and restarted when debounced function is called again. Look at the example diagram below.

https://raw.githubusercontent.com/javascript-tutorial/en.javascript.info/master/1-js/06-advanced-functions/09-call-apply-decorators/03-debounce/debounce.svg © Ilya Kantor(https://github.com/javascript-tutorial/en.javascript.info, licensed under CC-BY-NC)

The function f is a debounced function with a 1000ms timeout duration and is called at time instants 0, 200ms, and 500ms with arguments a, b, and c, respectively. Because f is debounced, calls f(a) and f(b) were "not committed/ignored" because there was another call to f within a 1000ms duration. Still, call f(c) was "committed/accepted" at the time instant 1500ms because no further call followed within 1000ms.

To implement this, you can use the setTimeout and clearTimeout functions. The setTimeout function accepts an action(code or function to execute) and delay in milliseconds, then returns a timer ID in integer. The given action will be executed when the timer expires without being canceled.

const timerId = setTimeout(action, delay)

The clearTimeout function could then be used to destroy the timer with a given ID.

clearTimeout(timerId)

Following simple debounce implementation could be used:

// Default timeout is set to 1000ms
function debounce(func, timeout = 1000) {
    // A slot to save timer id for current debounced function
    let timer
    // Return a function that conditionally calls the original function
    return (...args) => {
        // Immediately cancel the timer when called
        clearTimeout(timer)
        // Start another timer that will call the original function
        // unless canceled by following call
        timer = setTimeout(() => {
            // Pass all arguments and `this` value
            func.apply(this, args)
        }, timeout)
    }
}

Read more: Default parameters, Rest parameters, Function.apply(), this keyword

To use is quite simple:

eventTarget.addEventListener('wheel', debounce((e) => {
    console.log('wheel', e)
}))

This will limit console.log calls to whenever a wheel event has not been fired in a second.

Live example:

function debounce(f, d = 99, t) {
    return (...a) => {
        clearTimeout(t)
        t = setTimeout(() => {
            f.apply(this, a)
        }, d)
    }
}

document.addEventListener('wheel', debounce((_) => {
    console.log('wheel')
}))

A more modern approach uses Promise on top of this idea.

Monday Fatigue
  • 223
  • 1
  • 3
  • 19
0

You could set a minimum amount of time that must pass before you consider an additional scroll event as actionable.

For example, below, 3 seconds must pass between scroll events before console.log("wheel") is fired again:

function createScrollEventHandler(milliseconds)
{
  let allowed = true;
  return (event)=>
  {
        event.preventDefault();
        if (allowed)
        {
          console.log("wheel");
          allowed = false;
          setTimeout(()=>
          {
            allowed = true;
          },milliseconds);
        }  
  }
}
let scrollEventHandler = createScrollEventHandler(3000); // 3 seconds
window.addEventListener("wheel",scrollEventHandler);
Lonnie Best
  • 9,936
  • 10
  • 57
  • 97
  • thanks. But I need to be able to catch the next scroll event before 3s – tit Sep 23 '19 at 13:41
  • @tit : Look where it says createScrollEventHandler(3000). You can change 3000 to the number of milliseconds you desire. 1000 milliseconds = 1 second, for example. I just chose 3000 as an example. – Lonnie Best Sep 23 '19 at 14:56
  • I am interested in a no delay solution, just want to cancel the current wheel event – tit Sep 23 '19 at 15:08
  • @tit : If you need to cancel the event completely, put `return false;` right after `event.preventDefault();`. I'm still not clear on exactly what you're trying to accomplish. – Lonnie Best Sep 23 '19 at 16:46
  • Thanks. But this is not working either, see https://codepen.io/thipages/pen/VwZgmdR – tit Sep 23 '19 at 19:11
  • What exactly are you expectations? What do you think that code is suppose to do? – Lonnie Best Sep 23 '19 at 19:53
  • I want to allow scrolling from page to page only - with an animation while scrolling. As soon I detect the onwheel event, I would like to stop it before the animation finishes, otherwise the previous onwheel continues to fire and it is seen as new event, so going to the next of the targeted page – tit Sep 23 '19 at 23:29
0

You almost had it But you need to wrap your code in a function. I added some extra little bits so you can differentiate up and down :)

//scroll wheel manipulation
  window.addEventListener('wheel', function (e) {
    //TODO add delay
    if (e.deltaY < 0) {
      //scroll wheel up
      console.log("up");
    }
    if (e.deltaY > 0) {
      //scroll wheel down
      console.log("down");
    }
  });

How it works?

(e) = This is just the event, the function is triggered when ever you scroll up and down, but without the function event it just doesn't know what to do! Normally people put "event" but im lazy.

deltaY = This is a function of the wheel scroll it just makes sure you scrolling along the Y axis. Its a standard inbuilt function there is no external variables you need to add.

Extras

setTimeout

You could add this. In the if statements as @Lonnie Best suggested

Scott Chambers
  • 581
  • 6
  • 22