jQuery Tools Scrollable with Mousewheel - scroll ONE position and stop
Asked Answered
U

4

7

I am using bind/unbind for mousewheel scrolling, based on this SO response:

Jquery, unbinding mousewheel event, then rebinding it after actions are completed?

I'm tunneling up the event tree from delta, to target only the X mousewheel values. All is working well. The problem I'm trying to overcome: I want to simply scroll forward/backward ONE panel, then stop scrolling. Currently, I'm unbinding the mousewheel event immediately after the move, and that effectively stops the scrolling...but unbinding the mousewheel event also jacks the page. What I need is to be able to sniff the very first deltaX value for direction, then make a move and stop listening. Do I need to look to autoscroll for answers? Binding/unbinding feels klugy, but I can't, for the life of me, figure how to kick out after one move, while still then being able to scroll after that move is complete. Here's my mousewheel function:

function mouseHandler(event, delta) {
$('.homeScrollable').bind('mousewheel', mouseHandler);

var deltaX = event.originalEvent.wheelDeltaX;
//console.log(event.originalEvent.wheelDeltaX);

if(deltaX <= 0) {
    //move forward 1 screen and stop
    scrollapi.move(1);
    $('.homeScrollable').unbind('mousewheel', mouseHandler);
} else if(deltaX > 0) {
    //move backward 1 screen and stop
    scrollapi.move(-1);
    $('.homeScrollable').unbind('mousewheel', mouseHandler);
}   
event.preventDefault();

// Rebind mousewheel event:
$('.homeScrollable').bind('mousewheel', mouseHandler);     };

I've also looked at setting a timer, a la:

jquery mousewheel plugin: how to fire only one function every scroll

which seemed incredibly promising, but no-go. Here's the plugin page for this guy: http://brandonaaron.net/code/mousewheel/docs

Thanks for checking it out.

Unwarranted answered 4/1, 2012 at 16:36 Comment(0)
P
19

Since the DOM doesn't offer any way of differentiating between the first scrolling event fired and subsequent ones that happen to be part of the same scrolling motion, we're forced to think about indirect methods of distinguishing between them.

If you rapidly scroll through any particular element, a scrolling event is fired many times in sequence. Using the following code, we can get an idea about exactly how often that happens:

$('#exampleDiv').bind('mousewheel', function () {
    console.log(new Date().getTime());
});

When you scroll over that div, you'll get a console output that looks like this:

// Using mouse wheelbar
251327626600149
251327626600215
251327626600265
251327626600282
251327626600332
251327626600365

// Using touchpad
251327626626207
251327626626225
251327626626261
251327626626276
251327626626312
251327626626345

Looking at this output, it seems like the mousescroll events are generally fired within somewhere between 20 ms and 60 ms of each other. To be safe, we'll take the upper end to be 100 ms. This is very informative, because we can use it to distinguish between scrolling events that are part of the same action and ones that are likely distinct and deliberately initiated by the user.

What you could do from here is to make a globally accessible 'timestamp' variable, updating it every time a mousescroll event is fired, whether successful or not. Something like this:

var timeStamp = new Date().getTime();

$('#exampleDiv').bind('mousewheel', function (event) {
    var timeNow = new Date().getTime();

    // Need to prevent the default scrolling behavior
    event.preventDefault();

    // If the last mousescroll happened less that 100 ms ago, update
    // timeStamp and do nothing
    if (timeNow - timeStamp < 100) {
        timeStamp = timeNow;
        return;
    } else {
        timeStamp = timeNow;
        scrollToSomeOtherDiv();
    }
});

This effectively ignores all mousescroll events that are fired after the initial event that preceded them all but starts working again after the user has paused for 100 ms.

This would solve your problem, except if your scrollToSomeOtherDiv() function involved some kind of time-consuming animation. You could of course make a global boolean isAnimating, and check to see if it's true every time a mousescroll event is fired (making sure to turn it false in a callback once the animation is over).

That would work, except that it could provide a jarring experience for the user. Someone who wants to scroll through two panels quickly will likely not pause between scrolls even after seeing the animation begin. The code above will see all their mousescroll events as part of the same scrolling motion and continue to ignore them!

In that case, you could simply use the animation time as your threshold. You set a timeStamp once the animation is begun and then ignore all mousescroll events during that period of time. I've written up an example here: http://jsfiddle.net/Sg8JQ/

The relevant code is here:

var lastAnimation = 0;
var animationTime = 1000; // in ms
var quietPeriod = 500; // in ms, time after animation to ignore mousescroll

function scrollThis(event, delta, deltaX, deltaY) {
    var timeNow = new Date().getTime();

    // change this to deltaX/deltaY depending on which
    // scrolling dir you want to capture
    deltaOfInterest = deltaY;

    if (deltaOfInterest == 0) {
        // Uncomment if you want to use deltaX
        // event.preventDefault();
        return;
    }

    // Cancel scroll if currently animating or within quiet period
    if(timeNow - lastAnimation < quietPeriod + animationTime) {
        event.preventDefault();
        return;
    }

    if (deltaOfInterest < 0) {
        if ($('.active').next('div').length) {
            lastAnimation = timeNow;
            $('.active').removeClass('active').next('div').addClass('active');
            $('html,body').animate( {
                scrollTop: $('.active').offset().top }, animationTime);
        }
    } else {
        if ($('.active').prev('div').length) {
            lastAnimation = timeNow;
            $('.active').removeClass('active').prev('div').addClass('active');
            $('html,body').animate( {
                scrollTop: $('.active').offset().top }, animationTime);
        }
    }
}

// Note: mousewheel() is defined in the mousewheel plugin by Brandon Aaron
// You could do without it, but you'd need to adjust for firefox and webkit
// separately.
//
// You couldn't use $(document).scroll() because it doesn't allow you to
// preventDefault(), which I use here.
$(document).mousewheel(scrollThis);

I've also included quietPeriod which is time beyond the animation time during which you want to continue to ignore mousescroll events. You can set it to 0 if you want the scroll to be 'responsive' as soon as the animation is complete.

Peppercorn answered 27/1, 2012 at 2:18 Comment(7)
Every solution I devised involved setting some sort of delay. For the project in question, design mods eventually negated the issue, but it has still plagued me. Just yesterday I had a problem in the same ballpark - not similar - wherein I needed to stop an animate() on window resize. It put my mind back on this issue. I believe what you've provided to be the sexiest/likeliest thing I've come across to date. For that, I mark you the winner.Unwarranted
Awesome! Calling a piece of code 'sexy' is about as much a compliment as I can handle. I was thinking about your initial design idea though - with horizontal scrolling, you'd still run into some issues with gestures on mac (since two finger swipes both scroll and take you back in history on Chrome and Safari). I am yet to figure out a solution for that problem.Peppercorn
With values below 500 (talking about animationTime and quietPeriod), this pattern stops working. try again your jsfiddle with lowered values and you'll see the point where, in my mind, it stops working as expected. Solution appreciated if anyAccompanist
Hmm, so I've brought the values down (animationTime 200, quietPeriod 0), and it's working as expected for me. Could you be more specific as to how it's misbehaving for you?Peppercorn
It occurs to me that if your mouse or trackpad uses inertial scrolling (where scrolling continues once you've lifted your finger while the scrolling speed decays to zero), you could have unexpected behavior if the quietPeriod is too low. What would happen in that case is that you could move two div's down even though you "stopped" scrolling while the first scrolling animation was taking place. This is because the inertial scrolling did not have enough time to decay to zero and triggered another (unwanted) animation.Peppercorn
If that was indeed the problem, you could get around it by making sure quietPeriod is long enough, or by setting a threshold for deltaOfInterest below which you just don't trigger scroll events. That would allow you to get away with shorter values for quietPeriod.Peppercorn
really really Thanks :) , its helps me a lot.Destiny
L
1

I think the solution provided by "pikappa" is pure sexyness.

For me it was working perfectly on firefox but i was experiencing "flickering" while scrolling under Chrome 26.0.x

A simple dirty "patch" to his amazing code is adding "return false;" at line 57 and 64 of his JSfiddle

Lavonna answered 28/4, 2013 at 23:31 Comment(0)
I
0

Take a look at this and see what you think. To start I wouldn't bind to mousewheel because Firefox uses DOMMouseScroll instead. This also uses a different delta handler which is the inverse of the one used by all other browsers. Instead bind to jQuery's scroll event which normalizes the behavior. You can track the last scrolltop position to determine whether user scrolled up or down.

I am kind of making the assumption that you are animating between sections. Is there a callback function you can use to determine whether you should be scrolling? I created a global to track whether elements are currently animating. If so then we don't bother executing the function. Finally, I noticed that the callback for the scroll animation seems to trigger before the final scroll event occurs, so I actually had to use a setTimeout there. I didn't like it, but I couldn't figure out how else to get that callback to work properly.

http://jsfiddle.net/Gj3Qy/

var lastScrollTop = 0;
var isDoingStuff = false;

$(document).scroll(function(event) {
    //abandon 
    if(isDoingStuff) { return; }

    if ($(this).scrollTop() > lastScrollTop) {
        //console.log('down');
        $('.active').removeClass('active').next('div').addClass('active');
        isDoingStuff = true;
        $('html,body').animate( {scrollTop: $('.active').offset().top }, 1000, function() {
            setTimeout(function() {isDoingStuff = false;}, 100);
            console.log('off');
        });
    } else {
        //console.log('up');
    }
    lastScrollTop = $(this).scrollTop();
})
Irrecusable answered 4/1, 2012 at 17:6 Comment(4)
Your assumption is correct - I have a 1-page site, and am scrolling between sections - but the site/scrolling is horizontal. So I'd need to use scrollLeft instead of scrollTop, I guess. But the larger issue is that there is a property of the Tools Scrollable object called 'mousewheel' (flowplayer.org/tools/demos/scrollable/plugins/index.html), and if I do not use that (set to true or bind to the end), scrolling the mouse wheel nukes the site. Seems like there should be a way to postpone taking action (scrollable.move()) until the deltaX stops changing, then move once.Unwarranted
I wonder if .throttle() might be my magic bullet. benalman.com/code/projects/jquery-throttle-debounce/examples/…Unwarranted
Mental progress. I went in the direction you were headed, with a local boolean and timer. It wants to work, but the corner cases on Lion and my Mac mouse are beastly. I'm going to look at special mouse events for answers - specifically, scrollStop and scrollStart (james.padolsey.com/javascript/special-scroll-events-for-jquery).Unwarranted
Hey that's cool. If you sort something out I would love to see the final product.Irrecusable
K
0

So, this is what I am doing. It seems to work but is still buggy.

i=0;
$("#test").mousewheel(function(event, delta) {
    event.preventDefault();
    clearTimeout($.data(this, 'timer'));        

    // check if the mouse moved on right/up
    if( delta > 0 ) {               
        i++;                

        // hack to execute function just once
        if( i == 3 ) {                  
            $('#child').fadeIn().html("<h1>next</h1>").fadeOut();  // do something once when i=4, but happening multiple times
        }           

    } else { // check if mouse moved left/down

        i--;                

        if(i==-3) {     
            $('#child').fadeIn().html("<h1>previous</h1>").fadeOut();  // do something once when i=-4, but happening multiple times
        }               

    } // end if delta

    // only execute another mousewheel scroll after some delay
    $.data(this, 'timer', setTimeout(function() {
        i = 0;
    }, 100));

});

So, while the increased i prints out just fine, if I uncomment the if i = 4 print line, it doesn't work properly (you might have to scroll hard if you are using a mouse, i m trying in my trackpad). While I understand it as i continuously increases as you get delta value, you only execute something when i reaches 4 and only reset its value if the scroll is halted for 250ms, this code seems to reset i to 0 at least twice or thrice so i==4 actually executes twice or thrice. Perhaps you can find what might be happening. I m almost there, just need to fix that bug.

Try to add something that changes like fadeIn, fadeOut effect within that if(i==4) statement and its more prominent.

EDIT:

Heres the new edited version. The mousewheel link was old so perhaps this shows it better now. If you move fast, you see it flickering...

Kohler answered 7/1, 2012 at 2:8 Comment(1)
Is this supposed to be an answer to the OP or are you posting your own question? This is not a discussion forum.Ethical

© 2022 - 2024 — McMap. All rights reserved.