简体   繁体   中英

stopping mousewheel event from happening twice in OSX

I noticed mousewheel event is happening multiple times in mac osx. Can be atributed to inertia feature.

Is there a way to fix this behaviour?

(self signed ssl no worries please!) https://sandbox.idev.ge/roomshotel/html5_v2/

I'm using scrollSections.js https://github.com/guins/jQuery.scrollSections

And it uses mousewheel jquery plugin: https://github.com/brandonaaron/jquery-mousewheel

I'm seeing a lot of people having the same issue: https://github.com/brandonaaron/jquery-mousewheel/issues/36

There are some solutions but none works with scrollSections plugin.

Any ideas how to disable this inertia feature from JS?

My attempted fix:

// Fix for OSX inertia problem, jumping sections issue.
if (isMac) {

  var fireEvent;
  var newDelta = deltaY;

  if (oldDelta != null) {

    //check to see if they differ directions
    if (oldDelta < 0 && newDelta > 0) {
      fireEvent = true;
    }

    //check to see if they differ directions
    if (oldDelta > 0 && newDelta < 0) {
      fireEvent = true;
    }

    //check to see if they are the same direction
    if (oldDelta > 0 && newDelta > 0) {

      //check to see if the new is higher
      if (oldDelta < newDelta) {
        fireEvent = true;
      } else {
        fireEvent = false;
      }
    }

    //check to see if they are the same direction
    if (oldDelta < 0 && newDelta < 0) {

      //check to see if the new is lower
      if (oldDelta > newDelta) {
        fireEvent = true;
      } else {
        fireEvent = false;
      }
    }

  } else {

    fireEvent = true;

  }

  oldDelta = newDelta;

} else {

  fireEvent = true;

}

You can see fix implemented here: https://sandbox.idev.ge/roomshotel/html5_v2/ But it is a hit/miss.

The latest solution with timeouts had one major drawback: kinetic scrolling effect could last rather long (even 1s or so)... and disabling scrolling for 1-2 seconds wouldn't be the best decision.

Soooo, as promised, here's another approach.

Our goal is to provide one response for one user action, which in this case is scrolling.

What's 'one scrolling'? For the sake of solving this problem, let's say that 'one scrolling' is an event that lasts from the moment the page has started to move till the moment the movement has ended .

Kinetic scrolling effect is achieved by moving the page many times (say, every 20ms) for a small distance. It means that our kinetic scrolling consists of many-many little linear 'scrollings'.

Empirical testing has showed that this little 'scrollings' happen every 17-18ms in the middle of kinetic scroll, and about 80-90ms at the beginning and the end. Here's a simple test we can set up to see that:

var oldD;
var f = function(){
    var d = new Date().getTime();
    if(typeof oldD !== 'undefined')
        console.log(d-oldD);
    oldD = d;
}
window.onscroll=f;

Important! Every time this mini-scroll happens, scroll event is triggered. So:

 window.onscroll = function(){console.log("i'm scrolling!")};

will be fired 15 to 20+ times during one kinetic scroll. BTW, onscroll has really good browser support (see compatibility table ), so we can rely on it (except for touch devices, I'll cover this issue a bit later);

Some may say that redefining window.onscroll is not the best way to set event listeners. Yes, you're encouraged to use

 $(window).on('scroll',function(){...});

or whatever you like, it's not the point of the problem (I personally use my self-written library).

So, with the help of onscroll event we can reliably say whether this particular mini-movement of the page belongs to one long-lasting kinetic scroll, or is it a new one:

    var prevTime = new Date().getTime();
    var f = function(){
        var curTime = new Date().getTime();
        if(typeof prevTime !== 'undefined'){
            var timeDiff = curTime-prevTime;
            if(timeDiff>200)
                console.log('New kinetic scroll has started!');
        }
        prevTime = curTime;
    }
    window.onscroll=f;

Instead of "console.log" you can call your desired callback function (or event handler) and you're done! The function will be fired only once on every kinetic or simple scroll, which was our goal.

You may have noticed that I've used 200ms as a criteria of whether it's a new scroll or a part of the previous scroll. It's up to you to set it to greater values to be 999% sure you prevent any extra calls. However, please keep in mind that it's NOT what we have used in my previous answer. It's just a period of time between any two page movements (whether it's a new scroll or a little part of a kinetic scroll). To my mind, there's a very little chance that there will be a lag more than 200ms between steps in kinetic scroll (otherwise it will be not smooth at all).

As I've mentioned above, the onscroll event works differently on touch devices. It won't fire during every little step of kinetic scroll. But it will fire when the movement of the page has finally ended. Moreover, there's ontouchmove event... So, it's not a big deal. If necessary, I can provide solution for touch devices too.

PS I understand that I've written a bit too much, so I'd be happy to answer all your questions and provide further code if you need one .

Provided solution is supported in all browsers, very lightweight and, what's more important, is suitable not only for macs, but for every device that might implement kinetic scrolling, so I think it's really a way to go.

You know, I think it's a better idea to use timeouts in this case. Why not write something like this:

// Let's say it's a global context or whatever...:
var fireEvent = true;
var newDelta, oldDelta, eventTimeout;
newDelta = oldDelta = eventTimeout = null;

// ... and the function below fires onmousewheel or anything similar:
function someFunc(){
    if(!fireEvent) return; // if fireEvent is not allowed => stop execution here ('return' keyword stops execution of the function), else, execute code below:
    newDelta = deltaY;
    if(oldDelta!=null&&oldDelta*newDelta>0){ // (1.1) if it's not the first event and directions are the same => prevent possible dublicates for further 50ms:
        fireEvent = false;
        clearTimeout(eventTimeout); // clear previous timeouts. Important!
        eventTimeout = setTimeout(function(){fireEvent = true},500);
    }
    oldDelta = newDelta;
    someEventCallback(); // (1.2) fire further functions...
    }

So, any mousewheel event fired within half a second after any previous mousewheel event call will be ignored, if it is made in the same direction as previous (see condition at 1.1). It will solve the problem and there's no way user would spot this. Delay amount may be changed to better meet your needs.

The solution is made on pure JS. You're welcome to ask any questions about integrating it in your environment, but then I'll need you to provide further code of your page.

PS I have not seen anything similar to eventCallback() call in your code (see 1.2 of my solution). there was only fireEvent flag. Were you doing something like:

if(fireEvent)
    someEventCallback();

later on or something?

PPSnote that fireEvent should be in global scope in order to work here with setTimeout. If it's not, it's also quite easy to make it work fine, but the code needs to be altered a bit. If it's your case, tell me and I'll fix it for you.

UPDATE

After a brief search I found out, that similar mechanism is used in Underscore's _debounce() function. See Underscore documentation here

Have you though about using fullpage.js instead? It has a delay between arriving to a section and the moment you are able to scroll to the next section which solves part of the problem Mac users experience with track-pads or Apple magic mouses.

It would also provide you some other benefits, such as much more options, methods and compatibility with touch devices and old browsers with no CSS3 support.

To have something to start with, let's make your solution shorter (therefore easier to understand & debug):

var fireEvent;
var newDelta = deltaY;
var oldDelta = null;

fireEvent = EventCheck();
oldDelta = newDelta;

function EventCheck(){
    if(oldDelta==null) return true; //(1.1)
    if(oldDelta*newDelta < 0) return true; // (1.2) if directions differ => fire event
    if(Math.abs(newDelta)<Math.abs(oldDelta)) return true; // (1.3) if oldDelta exceeds newDelta in absolute values => fire event
    return false; // (1.4) else => don't fire;
}

As you see, it does absolutely what your code does. However, I can't understand this part of your code (which corresponds to (1.3) in my snippet):

 //check to see if the new is lower
 if (oldDelta > newDelta) {
    fireEvent = true;
 } else {
    fireEvent = false;
 }

from code provided it's unclear how deltaY is calculated. As one could assume, delta equals to endPosition - initialPosition. So, oldDelta>newDelta does not mean that the new position is lower, but that the new gap between these two values is bigger. If it's what it mean and you still use it, I suppose you try to track inertia with that. Then you should alter comparative operator (use less than, instead of greater then and vice-versa). In other words, I'd write:

if(Math.abs(newDelta)>Math.abs(oldDelta)) return true; // (1.3) 

you see, now I've used 'greater than' operator, which means: newDelta exceeds oldDelta in absolute values => it's not inertia and you can still fire the event.

Is it what you're trying to achieve or have I misinterpreted your code? If so, please explain how deltaY is calculated and what was your goal by comparing old&new Deltas. PS I'd suggest not to use if(isMac) in this step, while a problem can also potentially hide there.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM