tit
tit

Reputation: 619

wheel event PreventDefault does not cancel wheel event

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

Upvotes: 12

Views: 18107

Answers (4)

Monday Fatigue
Monday Fatigue

Reputation: 321

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.

Upvotes: 4

Mosè Raguzzini
Mosè Raguzzini

Reputation: 15831

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>

Upvotes: 5

Lonnie Best
Lonnie Best

Reputation: 11364

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

Upvotes: 0

Scott Chambers
Scott Chambers

Reputation: 611

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

Upvotes: 0

Related Questions